fix: add involves edge from task to agent:nomos at creation
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
ci / web (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled

Plus sync vendor directory for Docker build compatibility.
This commit is contained in:
2026-08-11 22:03:12 +02:00
parent 7d6a3320d4
commit febc153b7f
3384 changed files with 945212 additions and 2 deletions

View File

@@ -0,0 +1,410 @@
// Copyright (c) 2016, 2018, 2025, Oracle and/or its affiliates. All rights reserved.
// This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license.
package common
import (
"fmt"
"math/rand"
"net/http"
"os"
"strconv"
"sync"
"time"
"github.com/sony/gobreaker"
)
const (
// CircuitBreakerDefaultFailureRateThreshold is the requests failure rate which calculates in at most 120 seconds, once reaches to this rate, the circuit breaker state changes from closed to open
CircuitBreakerDefaultFailureRateThreshold float64 = 0.80
// CircuitBreakerDefaultClosedWindow is the default value of closeStateWindow, which is the cyclic period of the closed state
CircuitBreakerDefaultClosedWindow time.Duration = 120 * time.Second
// CircuitBreakerDefaultResetTimeout is the default value of openStateWindow, which is the wait time before setting the breaker to halfOpen state from open state
CircuitBreakerDefaultResetTimeout time.Duration = 30 * time.Second
// CircuitBreakerDefaultVolumeThreshold is the default value of minimumRequests in closed status
CircuitBreakerDefaultVolumeThreshold uint32 = 10
// DefaultCircuitBreakerName is the name of the circuit breaker
DefaultCircuitBreakerName string = "DefaultCircuitBreaker"
// DefaultCircuitBreakerServiceName is the servicename of the circuit breaker
DefaultCircuitBreakerServiceName string = ""
// DefaultCircuitBreakerHistoryCount is the default count of failed response history in circuit breaker
DefaultCircuitBreakerHistoryCount int = 5
// MinAuthClientCircuitBreakerResetTimeout is the min value of openStateWindow, which is the wait time before setting the breaker to halfOpen state from open state
MinAuthClientCircuitBreakerResetTimeout = 30
// MaxAuthClientCircuitBreakerResetTimeout is the max value of openStateWindow, which is the wait time before setting the breaker to halfOpen state from open state
MaxAuthClientCircuitBreakerResetTimeout = 49
// AuthClientCircuitBreakerName is the default circuit breaker name for the DefaultAuthClientCircuitBreakerSetting
AuthClientCircuitBreakerName = "FederationClientCircuitBreaker"
// AuthClientCircuitBreakerDefaultFailureThreshold is the default requests failure rate for the DefaultAuthClientCircuitBreakerSetting
AuthClientCircuitBreakerDefaultFailureThreshold float64 = 0.65
// AuthClientCircuitBreakerDefaultMinimumRequests is the default value of minimumRequests in closed status
AuthClientCircuitBreakerDefaultMinimumRequests uint32 = 3
)
// CircuitBreakerSetting wraps all exposed configurable params of circuit breaker
type CircuitBreakerSetting struct {
// Name is the Circuit Breaker's identifier
name string
// isEnabled is the switch of the circuit breaker, used for disable circuit breaker
isEnabled bool
// closeStateWindow is the cyclic period of the closed state, the default value is 120 seconds
closeStateWindow time.Duration
// openStateWindow is the wait time before setting the breaker to halfOpen state from open state, the default value is 30 seconds
openStateWindow time.Duration
// failureRateThreshold is the failure rate which calculates in at most closeStateWindow seconds, once reaches to this rate, the circuit breaker state changes from closed to open
// the circuit will transition from closed to open, the default value is 80%
failureRateThreshold float64
// minimumRequests is the minimum number of counted requests in closed state, the default value is 10 requests
minimumRequests uint32
// successStatCodeMap is the error(s) of StatusCode returned from service, which should be considered as the success or failure accounted by circuit breaker
// successStatCodeMap and successStatErrCodeMap are combined to use, if both StatusCode and ErrorCode are required, no need to add it to successStatCodeMap,
// the default value is [429, 500, 502, 503, 504]
successStatCodeMap map[int]bool
// successStatErrCodeMap is the error(s) of StatusCode and ErrorCode returned from service, which should be considered
// as the success or failure accounted by circuit breaker
// the default value is {409, "IncorrectState"}
successStatErrCodeMap map[StatErrCode]bool
// serviceName is the name of the service which can be set using withServiceName option for NewCircuitBreaker.
// the default value is empty string
serviceName string
// numberOfRecordedHistoryResponse is the number of failure responses stored in Circuit breaker history for debugging purpose
// the default value is 5
numberOfRecordedHistoryResponse int
}
// String Converts CircuitBreakerSetting to human-readable string representation
func (cbst CircuitBreakerSetting) String() string {
return fmt.Sprintf("{name=%v, isEnabled=%v, closeStateWindow=%v, openStateWindow=%v, failureRateThreshold=%v, minimumRequests=%v, successStatCodeMap=%v, successStatErrCodeMap=%v, serviceName=%v, historyCount=%v}",
cbst.name, cbst.isEnabled, cbst.closeStateWindow, cbst.openStateWindow, cbst.failureRateThreshold, cbst.minimumRequests, cbst.successStatCodeMap, cbst.successStatErrCodeMap, cbst.serviceName, cbst.numberOfRecordedHistoryResponse)
}
// ResponseHistory wraps the response params
type ResponseHistory struct {
timestamp time.Time
opcReqID string
errorCode string
errorMessage string
statusCode int
}
// String Converts ResponseHistory to human-readable string representation
func (rh ResponseHistory) String() string {
return fmt.Sprintf("Opc-Req-id - %v\nErrorCode - %v - %v\nErrorMessage - %v\n\n", rh.opcReqID, rh.statusCode, rh.errorCode, rh.errorMessage)
}
// AddToHistory processed the response and adds to response history queue
func (ocb *OciCircuitBreaker) AddToHistory(resp *http.Response, err ServiceError) {
respHist := new(ResponseHistory)
respHist.opcReqID = err.GetOpcRequestID()
respHist.errorCode = err.GetCode()
respHist.errorMessage = err.GetMessage()
respHist.statusCode = err.GetHTTPStatusCode()
respHist.timestamp, _ = time.Parse(time.RFC1123, resp.Header.Get("Date"))
ocb.historyQueueMutex.Lock()
defer ocb.historyQueueMutex.Unlock()
ocb.historyQueue = append(ocb.historyQueue, *respHist)
// cleaning up older values
if len(ocb.historyQueue) > ocb.Cbst.numberOfRecordedHistoryResponse {
// We have reached the capacity. Clean up the oldest value
ocb.historyQueue = ocb.historyQueue[1:]
}
for index := len(ocb.historyQueue) - 1; index >= 0; index-- {
if time.Since(ocb.historyQueue[index].timestamp) > ocb.Cbst.closeStateWindow {
// This response is older than the circuit breaker closeStateWindow.
// Remove all the older responses from 0 to index
ocb.historyQueue = ocb.historyQueue[index+1:]
break
}
}
return
}
// GetHistory processes the rsponse in queue to construct a String
func (ocb *OciCircuitBreaker) GetHistory() string {
getHistoryString := ""
ocb.historyQueueMutex.Lock()
defer ocb.historyQueueMutex.Unlock()
for _, value := range ocb.historyQueue {
getHistoryString += value.String()
}
return getHistoryString
}
// OciCircuitBreaker wraps all exposed configurable params of circuit breaker and 3P gobreaker CircuirBreaker
type OciCircuitBreaker struct {
Cbst *CircuitBreakerSetting
Cb *gobreaker.CircuitBreaker
historyQueue []ResponseHistory
historyQueueMutex sync.Mutex
}
// NewOciCircuitBreaker is used for initializing specified oci circuit breaker configuration with circuit breaker settings
func NewOciCircuitBreaker(cbst *CircuitBreakerSetting, gbcb *gobreaker.CircuitBreaker) *OciCircuitBreaker {
ocb := new(OciCircuitBreaker)
ocb.Cbst = cbst
if ocb.Cbst.numberOfRecordedHistoryResponse == 0 {
fmt.Println("num hist empty")
ocb.Cbst.numberOfRecordedHistoryResponse = getDefaultNumHistoryCount()
}
ocb.Cb = gbcb
ocb.historyQueue = make([]ResponseHistory, 0, ocb.Cbst.numberOfRecordedHistoryResponse)
return ocb
}
// CircuitBreakerOption is the type of the options for NewCircuitBreakerWithOptions.
type CircuitBreakerOption func(cbst *CircuitBreakerSetting)
// NewGoCircuitBreaker is a function to initialize a CircuitBreaker object with the specified configuration
// Add the interface, to allow the user directly use the 3P gobreaker.Setting's params.
func NewGoCircuitBreaker(st gobreaker.Settings) *gobreaker.CircuitBreaker {
return gobreaker.NewCircuitBreaker(st)
}
// DefaultCircuitBreakerSetting is used for set circuit breaker with default config
func DefaultCircuitBreakerSetting() *CircuitBreakerSetting {
successStatErrCodeMap := map[StatErrCode]bool{
{409, "IncorrectState"}: false,
}
successStatCodeMap := map[int]bool{
429: false,
500: false,
502: false,
503: false,
504: false,
}
return newCircuitBreakerSetting(
WithName(DefaultCircuitBreakerName),
WithIsEnabled(true),
WithCloseStateWindow(CircuitBreakerDefaultClosedWindow),
WithOpenStateWindow(CircuitBreakerDefaultResetTimeout),
WithFailureRateThreshold(CircuitBreakerDefaultFailureRateThreshold),
WithMinimumRequests(CircuitBreakerDefaultVolumeThreshold),
WithSuccessStatErrCodeMap(successStatErrCodeMap),
WithSuccessStatCodeMap(successStatCodeMap),
WithHistoryCount(getDefaultNumHistoryCount()))
}
// DefaultCircuitBreakerSettingWithServiceName is used for set circuit breaker with default config
func DefaultCircuitBreakerSettingWithServiceName(servicename string) *CircuitBreakerSetting {
successStatErrCodeMap := map[StatErrCode]bool{
{409, "IncorrectState"}: false,
}
successStatCodeMap := map[int]bool{
429: false,
500: false,
502: false,
503: false,
504: false,
}
return newCircuitBreakerSetting(
WithName(DefaultCircuitBreakerName),
WithIsEnabled(true),
WithCloseStateWindow(CircuitBreakerDefaultClosedWindow),
WithOpenStateWindow(CircuitBreakerDefaultResetTimeout),
WithFailureRateThreshold(CircuitBreakerDefaultFailureRateThreshold),
WithMinimumRequests(CircuitBreakerDefaultVolumeThreshold),
WithSuccessStatErrCodeMap(successStatErrCodeMap),
WithSuccessStatCodeMap(successStatCodeMap),
WithServiceName(servicename),
WithHistoryCount(getDefaultNumHistoryCount()))
}
// NoCircuitBreakerSetting is used for disable Circuit Breaker
func NoCircuitBreakerSetting() *CircuitBreakerSetting {
return NewCircuitBreakerSettingWithOptions(WithIsEnabled(false))
}
// NewCircuitBreakerSettingWithOptions is a helper method to assemble a CircuitBreakerSetting object.
// It starts out with the values returned by defaultCircuitBreakerSetting().
func NewCircuitBreakerSettingWithOptions(opts ...CircuitBreakerOption) *CircuitBreakerSetting {
cbst := DefaultCircuitBreakerSettingWithServiceName(DefaultCircuitBreakerServiceName)
// allow changing values
for _, opt := range opts {
opt(cbst)
}
if defaultLogger != nil && defaultLogger.LogLevel() == verboseLogging {
Debugf("Circuit Breaker setting: %s\n", cbst.String())
}
return cbst
}
// NewCircuitBreaker is used for initialing specified circuit breaker configuration with base client
func NewCircuitBreaker(cbst *CircuitBreakerSetting) *OciCircuitBreaker {
if !cbst.isEnabled {
return nil
}
st := gobreaker.Settings{}
customizeGoBreakerSetting(&st, cbst)
gbcb := gobreaker.NewCircuitBreaker(st)
return NewOciCircuitBreaker(cbst, gbcb)
}
func newCircuitBreakerSetting(opts ...CircuitBreakerOption) *CircuitBreakerSetting {
cbSetting := CircuitBreakerSetting{}
// allow changing values
for _, opt := range opts {
opt(&cbSetting)
}
return &cbSetting
}
// customizeGoBreakerSetting is used for converting CircuitBreakerSetting to 3P gobreaker's setting type
func customizeGoBreakerSetting(st *gobreaker.Settings, cbst *CircuitBreakerSetting) {
st.Name = cbst.name
st.Timeout = cbst.openStateWindow
st.Interval = cbst.closeStateWindow
st.OnStateChange = func(name string, from gobreaker.State, to gobreaker.State) {
if to == gobreaker.StateOpen {
Debugf("Circuit Breaker %s is now in Open State\n", name)
}
}
st.ReadyToTrip = func(counts gobreaker.Counts) bool {
failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
return counts.Requests >= cbst.minimumRequests && failureRatio >= cbst.failureRateThreshold
}
st.IsSuccessful = func(err error) bool {
if serviceErr, ok := IsServiceError(err); ok {
if isSuccessful, ok := cbst.successStatCodeMap[serviceErr.GetHTTPStatusCode()]; ok {
return isSuccessful
}
if isSuccessful, ok := cbst.successStatErrCodeMap[StatErrCode{serviceErr.GetHTTPStatusCode(), serviceErr.GetCode()}]; ok {
return isSuccessful
}
}
return true
}
}
// WithName is the option for NewCircuitBreaker that sets the Name.
func WithName(name string) CircuitBreakerOption {
// this is the CircuitBreakerOption function type
return func(cbst *CircuitBreakerSetting) {
cbst.name = name
}
}
// WithIsEnabled is the option for NewCircuitBreaker that sets the isEnabled.
func WithIsEnabled(isEnabled bool) CircuitBreakerOption {
// this is the CircuitBreakerOption function type
return func(cbst *CircuitBreakerSetting) {
cbst.isEnabled = isEnabled
}
}
// WithCloseStateWindow is the option for NewCircuitBreaker that sets the closeStateWindow.
func WithCloseStateWindow(window time.Duration) CircuitBreakerOption {
// this is the CircuitBreakerOption function type
return func(cbst *CircuitBreakerSetting) {
cbst.closeStateWindow = window
}
}
// WithOpenStateWindow is the option for NewCircuitBreaker that sets the openStateWindow.
func WithOpenStateWindow(window time.Duration) CircuitBreakerOption {
// this is the CircuitBreakerOption function type
return func(cbst *CircuitBreakerSetting) {
cbst.openStateWindow = window
}
}
// WithFailureRateThreshold is the option for NewCircuitBreaker that sets the failureRateThreshold.
func WithFailureRateThreshold(threshold float64) CircuitBreakerOption {
// this is the CircuitBreakerOption function type
return func(cbst *CircuitBreakerSetting) {
cbst.failureRateThreshold = threshold
}
}
// WithMinimumRequests is the option for NewCircuitBreaker that sets the minimumRequests.
func WithMinimumRequests(num uint32) CircuitBreakerOption {
// this is the CircuitBreakerOption function type
return func(cbst *CircuitBreakerSetting) {
cbst.minimumRequests = num
}
}
// WithSuccessStatCodeMap is the option for NewCircuitBreaker that sets the successStatCodeMap.
func WithSuccessStatCodeMap(successStatCodeMap map[int]bool) CircuitBreakerOption {
// this is the CircuitBreakerOption function type
return func(cbst *CircuitBreakerSetting) {
cbst.successStatCodeMap = successStatCodeMap
}
}
// WithSuccessStatErrCodeMap is the option for NewCircuitBreaker that sets the successStatErrCodeMap.
func WithSuccessStatErrCodeMap(successStatErrCodeMap map[StatErrCode]bool) CircuitBreakerOption {
// this is the CircuitBreakerOption function type
return func(cbst *CircuitBreakerSetting) {
cbst.successStatErrCodeMap = successStatErrCodeMap
}
}
// WithServiceName is the option for NewCircuitBreaker that sets the ServiceName.
func WithServiceName(serviceName string) CircuitBreakerOption {
// this is the CircuitBreakerOption function type
return func(cbst *CircuitBreakerSetting) {
cbst.serviceName = serviceName
}
}
// WithHistoryCount to set the number of failed responses
func WithHistoryCount(count int) CircuitBreakerOption {
// this is the CircuitBreakerOption function type
return func(cbst *CircuitBreakerSetting) {
cbst.numberOfRecordedHistoryResponse = count
}
}
// getDefaultNumHistoryCount to set the number of failed responses
func getDefaultNumHistoryCount() int {
if val, isSet := os.LookupEnv(circuitBreakerNumberOfHistoryResponseEnv); isSet {
count, err := strconv.Atoi(val)
if err == nil && count > 0 {
return count
}
Debugf("Invalid history count specified. Resetting to default value")
}
return DefaultCircuitBreakerHistoryCount
}
// GlobalCircuitBreakerSetting is global level circuit breaker setting, it would impact all services, the precedence is lower
// than client level circuit breaker
var GlobalCircuitBreakerSetting *CircuitBreakerSetting = nil
// ConfigCircuitBreakerFromEnvVar is used for checking the circuit breaker environment variable setting, default value is nil
func ConfigCircuitBreakerFromEnvVar(baseClient *BaseClient) {
if IsEnvVarTrue(isDefaultCircuitBreakerEnabled) {
baseClient.Configuration.CircuitBreaker = NewCircuitBreaker(DefaultCircuitBreakerSetting())
return
}
if IsEnvVarFalse(isDefaultCircuitBreakerEnabled) {
baseClient.Configuration.CircuitBreaker = nil
}
}
// ConfigCircuitBreakerFromGlobalVar is used for checking if global circuitBreakerSetting is configured, the priority is higher than cb env var
func ConfigCircuitBreakerFromGlobalVar(baseClient *BaseClient) {
if GlobalCircuitBreakerSetting != nil {
baseClient.Configuration.CircuitBreaker = NewCircuitBreaker(GlobalCircuitBreakerSetting)
}
}
// DefaultAuthClientCircuitBreakerSetting returns the default circuit breaker setting for the Auth Client
func DefaultAuthClientCircuitBreakerSetting() *CircuitBreakerSetting {
return NewCircuitBreakerSettingWithOptions(
WithOpenStateWindow(time.Duration(rand.Intn(MaxAuthClientCircuitBreakerResetTimeout+1-MinAuthClientCircuitBreakerResetTimeout)+MinAuthClientCircuitBreakerResetTimeout)*time.Second),
WithName(AuthClientCircuitBreakerName),
WithFailureRateThreshold(AuthClientCircuitBreakerDefaultFailureThreshold),
WithMinimumRequests(AuthClientCircuitBreakerDefaultMinimumRequests),
)
}
// GlobalAuthClientCircuitBreakerSetting is global level circuit breaker setting for the Auth Client
// than client level circuit breaker
var GlobalAuthClientCircuitBreakerSetting *CircuitBreakerSetting = nil

View File

@@ -0,0 +1,748 @@
// Copyright (c) 2016, 2018, 2025, Oracle and/or its affiliates. All rights reserved.
// This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license.
// Package common provides supporting functions and structs used by service packages
package common
import (
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"math/rand"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/user"
"path"
"path/filepath"
"reflect"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
)
const (
// DefaultHostURLTemplate The default url template for service hosts
DefaultHostURLTemplate = "%s.%s.oraclecloud.com"
// requestHeaderAccept The key for passing a header to indicate Accept
requestHeaderAccept = "Accept"
// requestHeaderAuthorization The key for passing a header to indicate Authorization
requestHeaderAuthorization = "Authorization"
// requestHeaderContentLength The key for passing a header to indicate Content Length
requestHeaderContentLength = "Content-Length"
// requestHeaderContentType The key for passing a header to indicate Content Type
requestHeaderContentType = "Content-Type"
// requestHeaderExpect The key for passing a header to indicate Expect/100-Continue
requestHeaderExpect = "Expect"
// requestHeaderDate The key for passing a header to indicate Date
requestHeaderDate = "Date"
// requestHeaderIfMatch The key for passing a header to indicate If Match
requestHeaderIfMatch = "if-match"
// requestHeaderOpcClientInfo The key for passing a header to indicate OPC Client Info
requestHeaderOpcClientInfo = "opc-client-info"
// requestHeaderOpcRetryToken The key for passing a header to indicate OPC Retry Token
requestHeaderOpcRetryToken = "opc-retry-token"
// requestHeaderOpcRequestID The key for unique Oracle-assigned identifier for the request.
requestHeaderOpcRequestID = "opc-request-id"
// requestHeaderOpcClientRequestID The key for unique Oracle-assigned identifier for the request.
requestHeaderOpcClientRequestID = "opc-client-request-id"
// requestHeaderUserAgent The key for passing a header to indicate User Agent
requestHeaderUserAgent = "User-Agent"
// requestHeaderXContentSHA256 The key for passing a header to indicate SHA256 hash
requestHeaderXContentSHA256 = "X-Content-SHA256"
// requestHeaderOpcOboToken The key for passing a header to use obo token
requestHeaderOpcOboToken = "opc-obo-token"
// private constants
defaultScheme = "https"
defaultSDKMarker = "Oracle-GoSDK"
defaultUserAgentTemplate = "%s/%s (%s/%s; go/%s)" //SDK/SDKVersion (OS/OSVersion; Lang/LangVersion)
// http.Client.Timeout includes Dial, TLSHandshake, Request, Response header and body
defaultTimeout = 60 * time.Second
defaultConfigFileName = "config"
defaultConfigDirName = ".oci"
configFilePathEnvVarName = "OCI_CONFIG_FILE"
secondaryConfigDirName = ".oraclebmc"
maxBodyLenForDebug = 1024 * 1000
// appendUserAgentEnv The key for retrieving append user agent value from env var
appendUserAgentEnv = "OCI_SDK_APPEND_USER_AGENT"
// requestHeaderOpcClientRetries The key for passing a header to set client retries info
requestHeaderOpcClientRetries = "opc-client-retries"
// isDefaultRetryEnabled The key for set default retry disabled from env var
isDefaultRetryEnabled = "OCI_SDK_DEFAULT_RETRY_ENABLED"
// isDefaultCircuitBreakerEnabled is the key for set default circuit breaker disabled from env var
isDefaultCircuitBreakerEnabled = "OCI_SDK_DEFAULT_CIRCUITBREAKER_ENABLED"
//circuitBreakerNumberOfHistoryResponseEnv is the number of recorded history responses
circuitBreakerNumberOfHistoryResponseEnv = "OCI_SDK_CIRCUITBREAKER_NUM_HISTORY_RESPONSE"
// ociDefaultRefreshIntervalForCustomCerts is the env var for overriding the defaultRefreshIntervalForCustomCerts.
// The value represents the refresh interval in minutes and has a higher precedence than defaultRefreshIntervalForCustomCerts
// but has a lower precedence then the refresh interval configured via OciGlobalRefreshIntervalForCustomCerts
// If the value is negative, then it is assumed that this property is not configured
// if the value is Zero, then the refresh of custom certs will be disabled
ociDefaultRefreshIntervalForCustomCerts = "OCI_DEFAULT_REFRESH_INTERVAL_FOR_CUSTOM_CERTS"
// ociDefaultCertsPath is the env var for the path to the SSL cert file
ociDefaultCertsPath = "OCI_DEFAULT_CERTS_PATH"
// ociDefaultClientCertsPath is the env var for the path to the custom client cert
ociDefaultClientCertsPath = "OCI_DEFAULT_CLIENT_CERTS_PATH"
// ociDefaultClientCertsPrivateKeyPath is the env var for the path to the custom client cert private key
ociDefaultClientCertsPrivateKeyPath = "OCI_DEFAULT_CLIENT_CERTS_PRIVATE_KEY_PATH"
//maxAttemptsForRefreshableRetry is the number of retry when 401 happened on a refreshable auth type
maxAttemptsForRefreshableRetry = 3
//defaultRefreshIntervalForCustomCerts is the default refresh interval in minutes
defaultRefreshIntervalForCustomCerts = 30
)
// OciGlobalRefreshIntervalForCustomCerts is the global policy for overriding the refresh interval in minutes.
// This variable has a higher precedence than the env variable OCI_DEFAULT_REFRESH_INTERVAL_FOR_CUSTOM_CERTS
// and the defaultRefreshIntervalForCustomCerts values.
// If the value is negative, then it is assumed that this property is not configured
// if the value is Zero, then the refresh of custom certs will be disabled
var OciGlobalRefreshIntervalForCustomCerts int = -1
// RequestInterceptor function used to customize the request before calling the underlying service
type RequestInterceptor func(*http.Request) error
// HTTPRequestDispatcher wraps the execution of a http request, it is generally implemented by
// http.Client.Do, but can be customized for testing
type HTTPRequestDispatcher interface {
Do(req *http.Request) (*http.Response, error)
}
// CustomClientConfiguration contains configurations set at client level, currently it only includes RetryPolicy
type CustomClientConfiguration struct {
RetryPolicy *RetryPolicy
CircuitBreaker *OciCircuitBreaker
RealmSpecificServiceEndpointTemplateEnabled *bool
}
// BaseClient struct implements all basic operations to call oci web services.
type BaseClient struct {
//HTTPClient performs the http network operations
HTTPClient HTTPRequestDispatcher
//Signer performs auth operation
Signer HTTPRequestSigner
//A request interceptor can be used to customize the request before signing and dispatching
Interceptor RequestInterceptor
//The host of the service
Host string
//The user agent
UserAgent string
//Base path for all operations of this client
BasePath string
Configuration CustomClientConfiguration
}
// SetCustomClientConfiguration sets client with retry and other custom configurations
func (client *BaseClient) SetCustomClientConfiguration(config CustomClientConfiguration) {
client.Configuration = config
}
// RetryPolicy returns the retryPolicy configured for client
func (client *BaseClient) RetryPolicy() *RetryPolicy {
return client.Configuration.RetryPolicy
}
// Endpoint returns the endpoint configured for client
func (client *BaseClient) Endpoint() string {
host := client.Host
if !strings.Contains(host, "http") &&
!strings.Contains(host, "https") {
host = fmt.Sprintf("%s://%s", defaultScheme, host)
}
return host
}
func defaultUserAgent() string {
userAgent := fmt.Sprintf(defaultUserAgentTemplate, defaultSDKMarker, Version(), runtime.GOOS, runtime.GOARCH, runtime.Version())
appendUA := os.Getenv(appendUserAgentEnv)
if appendUA != "" {
userAgent = fmt.Sprintf("%s %s", userAgent, appendUA)
}
return userAgent
}
var clientCounter int64
func getNextSeed() int64 {
newCounterValue := atomic.AddInt64(&clientCounter, 1)
return newCounterValue + time.Now().UnixNano()
}
func newBaseClient(signer HTTPRequestSigner, dispatcher HTTPRequestDispatcher) BaseClient {
rand.Seed(getNextSeed())
baseClient := BaseClient{
UserAgent: defaultUserAgent(),
Interceptor: nil,
Signer: signer,
HTTPClient: dispatcher,
}
// check the default retry environment variable setting
if IsEnvVarTrue(isDefaultRetryEnabled) {
defaultRetry := DefaultRetryPolicy()
baseClient.Configuration.RetryPolicy = &defaultRetry
} else if IsEnvVarFalse(isDefaultRetryEnabled) {
policy := NoRetryPolicy()
baseClient.Configuration.RetryPolicy = &policy
}
// check if user defined global retry is configured
if GlobalRetry != nil {
baseClient.Configuration.RetryPolicy = GlobalRetry
}
return baseClient
}
func defaultHTTPDispatcher() http.Client {
var httpClient http.Client
refreshInterval := getCustomCertRefreshInterval()
if refreshInterval <= 0 {
Debug("Custom cert refresh has been disabled")
}
var tp = &OciHTTPTransportWrapper{
RefreshRate: time.Duration(refreshInterval) * time.Minute,
TLSConfigProvider: GetTLSConfigTemplateForTransport(),
}
httpClient = http.Client{
Timeout: defaultTimeout,
Transport: tp,
}
return httpClient
}
func defaultBaseClient(provider KeyProvider) BaseClient {
dispatcher := defaultHTTPDispatcher()
signer := DefaultRequestSigner(provider)
return newBaseClient(signer, &dispatcher)
}
// DefaultBaseClientWithSigner creates a default base client with a given signer
func DefaultBaseClientWithSigner(signer HTTPRequestSigner) BaseClient {
dispatcher := defaultHTTPDispatcher()
return newBaseClient(signer, &dispatcher)
}
// NewClientWithConfig Create a new client with a configuration provider, the configuration provider
// will be used for the default signer as well as reading the region
// This function does not check for valid regions to implement forward compatibility
func NewClientWithConfig(configProvider ConfigurationProvider) (client BaseClient, err error) {
var ok bool
if ok, err = IsConfigurationProviderValid(configProvider); !ok {
err = fmt.Errorf("can not create client, bad configuration: %s", err.Error())
return
}
client = defaultBaseClient(configProvider)
if authConfig, e := configProvider.AuthType(); e == nil && authConfig.OboToken != nil {
Debugf("authConfig's authType is %s, and token content is %s", authConfig.AuthType, *authConfig.OboToken)
signOboToken(&client, *authConfig.OboToken, configProvider)
}
return
}
// NewClientWithOboToken Create a new client that will use oboToken for auth
func NewClientWithOboToken(configProvider ConfigurationProvider, oboToken string) (client BaseClient, err error) {
client, err = NewClientWithConfig(configProvider)
if err != nil {
return
}
signOboToken(&client, oboToken, configProvider)
return
}
// Add obo token header to Interceptor and sign to client
func signOboToken(client *BaseClient, oboToken string, configProvider ConfigurationProvider) {
// Interceptor to add obo token header
client.Interceptor = func(request *http.Request) error {
request.Header.Add(requestHeaderOpcOboToken, oboToken)
return nil
}
// Obo token will also be signed
defaultHeaders := append(DefaultGenericHeaders(), requestHeaderOpcOboToken)
client.Signer = RequestSigner(configProvider, defaultHeaders, DefaultBodyHeaders())
}
func getHomeFolder() string {
current, e := user.Current()
if e != nil {
//Give up and try to return something sensible
home := os.Getenv("HOME")
if home == "" {
home = os.Getenv("USERPROFILE")
}
return home
}
return current.HomeDir
}
// DefaultConfigProvider returns the default config provider. The default config provider
// will look for configurations in 3 places: file in $HOME/.oci/config, HOME/.obmcs/config and
// variables names starting with the string TF_VAR. If the same configuration is found in multiple
// places the provider will prefer the first one.
// If the config file is not placed in the default location, the environment variable
// OCI_CONFIG_FILE can provide the config file location.
func DefaultConfigProvider() ConfigurationProvider {
defaultConfigFile := getDefaultConfigFilePath()
homeFolder := getHomeFolder()
secondaryConfigFile := filepath.Join(homeFolder, secondaryConfigDirName, defaultConfigFileName)
defaultFileProvider, _ := ConfigurationProviderFromFile(defaultConfigFile, "")
secondaryFileProvider, _ := ConfigurationProviderFromFile(secondaryConfigFile, "")
environmentProvider := environmentConfigurationProvider{EnvironmentVariablePrefix: "TF_VAR"}
provider, _ := ComposingConfigurationProvider([]ConfigurationProvider{defaultFileProvider, secondaryFileProvider, environmentProvider})
Debugf("Configuration provided by: %s", provider)
return provider
}
// CustomProfileSessionTokenConfigProvider returns the session token config provider of the given profile.
// This will look for the configuration in the given config file path.
func CustomProfileSessionTokenConfigProvider(customConfigPath string, profile string) ConfigurationProvider {
if customConfigPath == "" {
customConfigPath = getDefaultConfigFilePath()
}
sessionTokenConfigurationProvider, _ := ConfigurationProviderForSessionTokenWithProfile(customConfigPath, profile, "")
Debugf("Configuration provided by: %s", sessionTokenConfigurationProvider)
return sessionTokenConfigurationProvider
}
func getDefaultConfigFilePath() string {
homeFolder := getHomeFolder()
defaultConfigFile := filepath.Join(homeFolder, defaultConfigDirName, defaultConfigFileName)
if _, err := os.Stat(defaultConfigFile); err == nil {
return defaultConfigFile
}
Debugf("The %s does not exist, will check env var %s for file path.", defaultConfigFile, configFilePathEnvVarName)
// Read configuration file path from OCI_CONFIG_FILE env var
fallbackConfigFile, existed := os.LookupEnv(configFilePathEnvVarName)
if !existed {
Debugf("The env var %s does not exist...", configFilePathEnvVarName)
return defaultConfigFile
}
if _, err := os.Stat(fallbackConfigFile); os.IsNotExist(err) {
Debugf("The specified cfg file path in the env var %s does not exist: %s", configFilePathEnvVarName, fallbackConfigFile)
return defaultConfigFile
}
return fallbackConfigFile
}
// setRawPath sets the Path and RawPath fields of the URL based on the provided
// escaped path p. It maintains the invariant that RawPath is only specified
// when it differs from the default encoding of the path.
// For example:
// - setPath("/foo/bar") will set Path="/foo/bar" and RawPath=""
// - setPath("/foo%2fbar") will set Path="/foo/bar" and RawPath="/foo%2fbar"
func setRawPath(u *url.URL) error {
oldPath := u.Path
path, err := url.PathUnescape(u.Path)
if err != nil {
return err
}
u.Path = path
if escp := u.EscapedPath(); oldPath == escp {
// Default encoding is fine.
u.RawPath = ""
} else {
u.RawPath = oldPath
}
return nil
}
// CustomProfileConfigProvider returns the config provider of given profile. The custom profile config provider
// will look for configurations in 2 places: file in $HOME/.oci/config, and variables names starting with the
// string TF_VAR. If the same configuration is found in multiple places the provider will prefer the first one.
func CustomProfileConfigProvider(customConfigPath string, profile string) ConfigurationProvider {
homeFolder := getHomeFolder()
if customConfigPath == "" {
customConfigPath = filepath.Join(homeFolder, defaultConfigDirName, defaultConfigFileName)
}
customFileProvider, _ := ConfigurationProviderFromFileWithProfile(customConfigPath, profile, "")
defaultFileProvider, _ := ConfigurationProviderFromFileWithProfile(customConfigPath, "DEFAULT", "")
environmentProvider := environmentConfigurationProvider{EnvironmentVariablePrefix: "TF_VAR"}
provider, _ := ComposingConfigurationProvider([]ConfigurationProvider{customFileProvider, defaultFileProvider, environmentProvider})
Debugf("Configuration provided by: %s", provider)
return provider
}
func (client *BaseClient) prepareRequest(request *http.Request) (err error) {
if client.UserAgent == "" {
return fmt.Errorf("user agent can not be blank")
}
if request.Header == nil {
request.Header = http.Header{}
}
request.Header.Set(requestHeaderUserAgent, client.UserAgent)
request.Header.Set(requestHeaderDate, time.Now().UTC().Format(http.TimeFormat))
if !strings.Contains(client.Host, "http") &&
!strings.Contains(client.Host, "https") {
client.Host = fmt.Sprintf("%s://%s", defaultScheme, client.Host)
}
clientURL, err := url.Parse(client.Host)
if err != nil {
return fmt.Errorf("host is invalid. %s", err.Error())
}
request.URL.Host = clientURL.Host
request.URL.Scheme = clientURL.Scheme
currentPath := request.URL.Path
if !strings.Contains(currentPath, fmt.Sprintf("/%s", client.BasePath)) {
request.URL.Path = path.Clean(fmt.Sprintf("/%s/%s", client.BasePath, currentPath))
err := setRawPath(request.URL)
if err != nil {
return err
}
}
return
}
func (client BaseClient) intercept(request *http.Request) (err error) {
if client.Interceptor != nil {
err = client.Interceptor(request)
}
return
}
// checkForSuccessfulResponse checks if the response is successful
// If Error Code is 4XX/5XX and debug level is set to info, will log the request and response
func checkForSuccessfulResponse(res *http.Response, requestBody *io.ReadCloser) error {
familyStatusCode := res.StatusCode / 100
if familyStatusCode == 4 || familyStatusCode == 5 {
IfInfo(func() {
// If debug level is set to verbose, the request and request body will be dumped and logged under debug level, this is to avoid duplicate logging
if defaultLogger.LogLevel() < verboseLogging {
logRequest(res.Request, Logf, noLogging)
if requestBody != nil && *requestBody != http.NoBody {
bodyContent, _ := ioutil.ReadAll(*requestBody)
Logf("Dump Request Body: \n%s", string(bodyContent))
}
}
logResponse(res, Logf, infoLogging)
})
return newServiceFailureFromResponse(res)
}
IfDebug(func() {
logResponse(res, Debugf, verboseLogging)
})
return nil
}
func logRequest(request *http.Request, fn func(format string, v ...interface{}), bodyLoggingLevel int) {
if request == nil {
return
}
dumpBody := true
if checkBodyLengthExceedLimit(request.ContentLength) {
fn("not dumping body too big\n")
dumpBody = false
}
dumpBody = dumpBody && defaultLogger.LogLevel() >= bodyLoggingLevel && bodyLoggingLevel != noLogging
if dump, e := httputil.DumpRequestOut(request, dumpBody); e == nil {
fn("Dump Request %s", string(dump))
} else {
fn("%v\n", e)
}
}
func logResponse(response *http.Response, fn func(format string, v ...interface{}), bodyLoggingLevel int) {
if response == nil {
return
}
dumpBody := true
if checkBodyLengthExceedLimit(response.ContentLength) {
fn("not dumping body too big\n")
dumpBody = false
}
dumpBody = dumpBody && defaultLogger.LogLevel() >= bodyLoggingLevel && bodyLoggingLevel != noLogging
if dump, e := httputil.DumpResponse(response, dumpBody); e == nil {
fn("Dump Response %s", string(dump))
} else {
fn("%v\n", e)
}
}
func checkBodyLengthExceedLimit(contentLength int64) bool {
return contentLength > maxBodyLenForDebug
}
// OCIRequest is any request made to an OCI service.
type OCIRequest interface {
// HTTPRequest assembles an HTTP request.
HTTPRequest(method, path string, binaryRequestBody *OCIReadSeekCloser, extraHeaders map[string]string) (http.Request, error)
}
// RequestMetadata is metadata about an OCIRequest. This structure represents the behavior exhibited by the SDK when
// issuing (or reissuing) a request.
type RequestMetadata struct {
// RetryPolicy is the policy for reissuing the request. If no retry policy is set on the request,
// then the request will be issued exactly once.
RetryPolicy *RetryPolicy
}
// OCIReadSeekCloser is a thread-safe io.ReadSeekCloser to prevent racing with retrying binary requests
type OCIReadSeekCloser struct {
rc io.ReadCloser
lock sync.Mutex
isClosed bool
}
// NewOCIReadSeekCloser constructs OCIReadSeekCloser, the only input is binary request body
func NewOCIReadSeekCloser(rc io.ReadCloser) *OCIReadSeekCloser {
rsc := OCIReadSeekCloser{}
rsc.rc = rc
return &rsc
}
// Seek is a thread-safe operation, it implements io.seek() interface, if the original request body implements io.seek()
// interface, or implements "well-known" data type like os.File, io.SectionReader, or wrapped by ioutil.NopCloser can be supported
func (rsc *OCIReadSeekCloser) Seek(offset int64, whence int) (int64, error) {
rsc.lock.Lock()
defer rsc.lock.Unlock()
if _, ok := rsc.rc.(io.Seeker); ok {
return rsc.rc.(io.Seeker).Seek(offset, whence)
}
// once the binary request body is wrapped with ioutil.NopCloser:
if isNopCloser(rsc.rc) {
unwrappedInterface := reflect.ValueOf(rsc.rc).Field(0).Interface()
if _, ok := unwrappedInterface.(io.Seeker); ok {
return unwrappedInterface.(io.Seeker).Seek(offset, whence)
}
}
return 0, fmt.Errorf("current binary request body type is not seekable, if want to use retry feature, please make sure the request body implements seek() method")
}
// Close is a thread-safe operation, it closes the instance of the OCIReadSeekCloser's access to the underlying io.ReadCloser.
func (rsc *OCIReadSeekCloser) Close() error {
rsc.lock.Lock()
defer rsc.lock.Unlock()
rsc.isClosed = true
return nil
}
// Read is a thread-safe operation, it implements io.Read() interface
func (rsc *OCIReadSeekCloser) Read(p []byte) (n int, err error) {
rsc.lock.Lock()
defer rsc.lock.Unlock()
if rsc.isClosed {
return 0, io.EOF
}
return rsc.rc.Read(p)
}
// Seekable is used for check if the binary request body can be seek or no
func (rsc *OCIReadSeekCloser) Seekable() bool {
if rsc == nil {
return false
}
if _, ok := rsc.rc.(io.Seeker); ok {
return true
}
// once the binary request body is wrapped with ioutil.NopCloser:
if isNopCloser(rsc.rc) {
if _, ok := reflect.ValueOf(rsc.rc).Field(0).Interface().(io.Seeker); ok {
return true
}
}
return false
}
// OCIResponse is the response from issuing a request to an OCI service.
type OCIResponse interface {
// HTTPResponse returns the raw HTTP response.
HTTPResponse() *http.Response
}
// OCIOperation is the generalization of a request-response cycle undergone by an OCI service.
type OCIOperation func(context.Context, OCIRequest, *OCIReadSeekCloser, map[string]string) (OCIResponse, error)
// ClientCallDetails a set of settings used by the a single Call operation of the http Client
type ClientCallDetails struct {
Signer HTTPRequestSigner
}
// Call executes the http request with the given context
func (client BaseClient) Call(ctx context.Context, request *http.Request) (response *http.Response, err error) {
if client.IsRefreshableAuthType() {
return client.RefreshableTokenWrappedCallWithDetails(ctx, request, ClientCallDetails{Signer: client.Signer})
}
return client.CallWithDetails(ctx, request, ClientCallDetails{Signer: client.Signer})
}
// RefreshableTokenWrappedCallWithDetails wraps the CallWithDetails with retry on 401 for Refreshable Toekn (Instance Principal, Resource Principal etc.)
// This is to intimitate the race condition on refresh
func (client BaseClient) RefreshableTokenWrappedCallWithDetails(ctx context.Context, request *http.Request, details ClientCallDetails) (response *http.Response, err error) {
for i := 0; i < maxAttemptsForRefreshableRetry; i++ {
response, err = client.CallWithDetails(ctx, request, ClientCallDetails{Signer: client.Signer})
if response != nil && response.StatusCode != 401 {
return response, err
}
time.Sleep(1 * time.Second)
}
return
}
// CallWithDetails executes the http request, the given context using details specified in the parameters, this function
// provides a way to override some settings present in the client
func (client BaseClient) CallWithDetails(ctx context.Context, request *http.Request, details ClientCallDetails) (response *http.Response, err error) {
Debugln("Attempting to call downstream service")
request = request.WithContext(ctx)
err = client.prepareRequest(request)
if err != nil {
return
}
//Intercept
err = client.intercept(request)
if err != nil {
return
}
//Sign the request
err = details.Signer.Sign(request)
if err != nil {
return
}
//Execute the http request
if ociGoBreaker := client.Configuration.CircuitBreaker; ociGoBreaker != nil {
resp, cbErr := ociGoBreaker.Cb.Execute(func() (interface{}, error) {
return client.httpDo(request)
})
if httpResp, ok := resp.(*http.Response); ok {
if httpResp != nil && httpResp.StatusCode != 200 {
if failure, ok := IsServiceError(cbErr); ok {
ociGoBreaker.AddToHistory(resp.(*http.Response), failure)
}
}
}
if cbErr != nil && IsCircuitBreakerError(cbErr) {
cbErr = getCircuitBreakerError(request, cbErr, ociGoBreaker)
}
if _, ok := resp.(*http.Response); !ok {
return nil, cbErr
}
return resp.(*http.Response), cbErr
}
return client.httpDo(request)
}
// IsRefreshableAuthType validates if a signer is from a refreshable config provider
func (client BaseClient) IsRefreshableAuthType() bool {
if signer, ok := client.Signer.(ociRequestSigner); ok {
if provider, ok := signer.KeyProvider.(RefreshableConfigurationProvider); ok {
return provider.Refreshable()
}
}
return false
}
func (client BaseClient) httpDo(request *http.Request) (response *http.Response, err error) {
//Copy request body and save for logging
dumpRequestBody := ioutil.NopCloser(bytes.NewBuffer(nil))
if request.Body != nil && !checkBodyLengthExceedLimit(request.ContentLength) {
if dumpRequestBody, request.Body, err = drainBody(request.Body); err != nil {
dumpRequestBody = ioutil.NopCloser(bytes.NewBuffer(nil))
}
}
IfDebug(func() {
logRequest(request, Debugf, verboseLogging)
})
//Execute the http request
response, err = client.HTTPClient.Do(request)
if err != nil {
IfInfo(func() {
Logf("%v\n", err)
})
return response, err
}
err = checkForSuccessfulResponse(response, &dumpRequestBody)
return response, err
}
// CloseBodyIfValid closes the body of an http response if the response and the body are valid
func CloseBodyIfValid(httpResponse *http.Response) {
if httpResponse != nil && httpResponse.Body != nil {
if httpResponse.Header != nil && strings.ToLower(httpResponse.Header.Get("content-type")) == "text/event-stream" {
return
}
httpResponse.Body.Close()
}
}
// IsOciRealmSpecificServiceEndpointTemplateEnabled returns true if the client is configured to use realm specific service endpoint template
// it will first check the client configuration, if not set, it will check the environment variable
func (client BaseClient) IsOciRealmSpecificServiceEndpointTemplateEnabled() bool {
if client.Configuration.RealmSpecificServiceEndpointTemplateEnabled != nil {
return *client.Configuration.RealmSpecificServiceEndpointTemplateEnabled
}
return IsEnvVarTrue(OciRealmSpecificServiceEndpointTemplateEnabledEnvVar)
}
func getCustomCertRefreshInterval() int {
if OciGlobalRefreshIntervalForCustomCerts >= 0 {
Debugf("Setting refresh interval as %d for custom certs via OciGlobalRefreshIntervalForCustomCerts", OciGlobalRefreshIntervalForCustomCerts)
return OciGlobalRefreshIntervalForCustomCerts
}
if refreshIntervalValue, ok := os.LookupEnv(ociDefaultRefreshIntervalForCustomCerts); ok {
refreshInterval, err := strconv.Atoi(refreshIntervalValue)
if err != nil || refreshInterval < 0 {
Debugf("The environment variable %s is not a valid int or is a negative value, skipping this configuration", ociDefaultRefreshIntervalForCustomCerts)
} else {
Debugf("Setting refresh interval as %d for custom certs via the env variable %s", refreshInterval, ociDefaultRefreshIntervalForCustomCerts)
return refreshInterval
}
}
Debugf("Setting the default refresh interval %d for custom certs", defaultRefreshIntervalForCustomCerts)
return defaultRefreshIntervalForCustomCerts
}

View File

@@ -0,0 +1,625 @@
// Copyright (c) 2016, 2018, 2025, Oracle and/or its affiliates. All rights reserved.
// This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license.
package common
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"time"
)
// Region type for regions
type Region string
const (
instanceMetadataRegionInfoURLV2 = "http://169.254.169.254/opc/v2/instance/regionInfo"
// Region Metadata Configuration File
regionMetadataCfgDirName = ".oci"
regionMetadataCfgFileName = "regions-config.json"
// Region Metadata Environment Variable
regionMetadataEnvVarName = "OCI_REGION_METADATA"
// Default Realm Environment Variable
defaultRealmEnvVarName = "OCI_DEFAULT_REALM"
//EndpointTemplateForRegionWithDot Environment Variable
EndpointTemplateForRegionWithDot = "https://{endpoint_service_name}.{region}"
// Region Metadata
regionIdentifierPropertyName = "regionIdentifier" // e.g. "ap-sydney-1"
realmKeyPropertyName = "realmKey" // e.g. "oc1"
realmDomainComponentPropertyName = "realmDomainComponent" // e.g. "oraclecloud.com"
regionKeyPropertyName = "regionKey" // e.g. "SYD"
// OciRealmSpecificServiceEndpointTemplateEnabledEnvVar is the environment variable name to enable the realm specific service endpoint template.
OciRealmSpecificServiceEndpointTemplateEnabledEnvVar = "OCI_REALM_SPECIFIC_SERVICE_ENDPOINT_TEMPLATE_ENABLED"
)
// External region metadata info flag, used to control adding these metadata region info only once.
var readCfgFile, readEnvVar, visitIMDS bool = true, true, false
// getRegionInfoFromInstanceMetadataService gets the region information
var getRegionInfoFromInstanceMetadataService = getRegionInfoFromInstanceMetadataServiceProd
// OciRealmSpecificServiceEndpointTemplateEnabled is the flag to enable the realm specific service endpoint template. This one has higher priority than the environment variable.
var OciRealmSpecificServiceEndpointTemplateEnabled *bool = nil
// OciSdkEnabledServicesMap is a list of services that are enabled, default is an empty list which means all services are enabled
var OciSdkEnabledServicesMap map[string]bool
// OciDeveloperToolConfigurationFilePathEnvVar is the environment variable name for the OCI Developer Tool Config File Path
const OciDeveloperToolConfigurationFilePathEnvVar = "OCI_DEVELOPER_TOOL_CONFIGURATION_FILE_PATH"
// OciAllowOnlyDeveloperToolConfigurationRegionsEnvVar is the environment variable name for the OCI Allow only Dev Tool Config Regions
const OciAllowOnlyDeveloperToolConfigurationRegionsEnvVar = "OCI_ALLOW_ONLY_DEVELOPER_TOOL_CONFIGURATION_REGIONS"
// defaultRealmForUnknownDeveloperToolConfigurationRegion is the default realm for unknown Developer Tool Configuration Regions
const defaultRealmForUnknownDeveloperToolConfigurationRegion = "oraclecloud.com"
// OciDeveloperToolConfigurationProvider is the provider name for the OCI Developer Tool Configuration file
var OciDeveloperToolConfigurationProvider string
// ociAllowOnlyDeveloperToolConfigurationRegions is the flag to enable the OCI Allow Only Developer Tool Configuration Regions. This one has lower priority than the environment variable.
var ociAllowOnlyDeveloperToolConfigurationRegions bool
var ociDeveloperToolConfigurationRegionSchemaList []map[string]string
// Endpoint returns a endpoint for a service
func (region Region) Endpoint(service string) string {
// Endpoint for dotted region
if strings.Contains(string(region), ".") {
return fmt.Sprintf("%s.%s", service, region)
}
return fmt.Sprintf("%s.%s.%s", service, region, region.SecondLevelDomain())
}
// EndpointForTemplate returns a endpoint for a service based on template, only unknown region name can fall back to "oc1", but not short code region name.
func (region Region) EndpointForTemplate(service string, serviceEndpointTemplate string) string {
if strings.Contains(string(region), ".") {
endpoint, error := region.EndpointForTemplateDottedRegion(service, serviceEndpointTemplate, "")
if error != nil {
Debugf("%v", error)
return ""
}
return endpoint
}
if serviceEndpointTemplate == "" {
return region.Endpoint(service)
}
// replace service prefix
endpoint := strings.Replace(serviceEndpointTemplate, "{serviceEndpointPrefix}", service, 1)
// replace region
endpoint = strings.Replace(endpoint, "{region}", string(region), 1)
// replace second level domain
endpoint = strings.Replace(endpoint, "{secondLevelDomain}", region.SecondLevelDomain(), 1)
return endpoint
}
// EndpointForTemplateDottedRegion returns a endpoint for a service based on the service name and EndpointTemplateForRegionWithDot template. If a service name is missing it is obtained from serviceEndpointTemplate and endpoint is constructed usingEndpointTemplateForRegionWithDot template.
func (region Region) EndpointForTemplateDottedRegion(service string, serviceEndpointTemplate string, endpointServiceName string) (string, error) {
if !strings.Contains(string(region), ".") {
var endpoint = ""
if serviceEndpointTemplate != "" {
endpoint = region.EndpointForTemplate(service, serviceEndpointTemplate)
return endpoint, nil
}
endpoint = region.EndpointForTemplate(service, "")
return endpoint, nil
}
if endpointServiceName != "" {
endpoint := strings.Replace(EndpointTemplateForRegionWithDot, "{endpoint_service_name}", endpointServiceName, 1)
endpoint = strings.Replace(endpoint, "{region}", string(region), 1)
Debugf("Constructing endpoint from service name %s and region %s. Endpoint: %s", endpointServiceName, region, endpoint)
return endpoint, nil
}
if serviceEndpointTemplate != "" {
var endpoint = ""
res := strings.Split(serviceEndpointTemplate, "//")
if len(res) > 1 {
res = strings.Split(res[1], ".")
if len(res) > 1 {
endpoint = strings.Replace(EndpointTemplateForRegionWithDot, "{endpoint_service_name}", res[0], 1)
endpoint = strings.Replace(endpoint, "{region}", string(region), 1)
Debugf("Constructing endpoint from service endpoint template %s and region %s. Endpoint: %s", serviceEndpointTemplate, region, endpoint)
} else {
return endpoint, fmt.Errorf("Endpoint service name not present in endpoint template")
}
} else {
return endpoint, fmt.Errorf("invalid serviceEndpointTemplates. ServiceEndpointTemplate should start with https://")
}
return endpoint, nil
}
return "", fmt.Errorf("EndpointForTemplateDottedRegion function requires endpointServiceName or serviceEndpointTemplate, no endpointServiceName or serviceEndpointTemplate provided")
}
func (region Region) SecondLevelDomain() string {
if realmID, ok := regionRealm[region]; ok {
if secondLevelDomain, ok := realm[realmID]; ok {
return secondLevelDomain
}
}
if value, ok := os.LookupEnv(defaultRealmEnvVarName); ok {
return value
}
Debugf("cannot find realm for region : %s, return default realm value.", region)
if _, ok := realm["oc1"]; !ok {
return defaultRealmForUnknownDeveloperToolConfigurationRegion
}
return realm["oc1"]
}
// RealmID is used for getting realmID from region, if no region found, directly throw error
func (region Region) RealmID() (string, error) {
if realmID, ok := regionRealm[region]; ok {
return realmID, nil
}
return "", fmt.Errorf("cannot find realm for region : %s", region)
}
// StringToRegion convert a string to Region type
func StringToRegion(stringRegion string) (r Region) {
regionStr := strings.ToLower(stringRegion)
// check for PLC related regions
if checkAllowOnlyDeveloperToolConfigurationRegions() && (checkDeveloperToolConfigurationFile() || len(ociDeveloperToolConfigurationRegionSchemaList) != 0) {
Debugf("Developer Tool config detected and OCI_ALLOW_ONLY_DEVELOPER_TOOL_CONFIGURATION_REGIONS is set to True, SDK will only use regions defined for Developer Tool Configuration Regions")
setRegionMetadataFromDeveloperToolConfigurationFile(&stringRegion)
if len(ociDeveloperToolConfigurationRegionSchemaList) != 0 {
resetRegionInfo()
bulkAddRegionSchema(ociDeveloperToolConfigurationRegionSchemaList)
}
r = Region(stringRegion)
if _, ok := regionRealm[r]; !ok {
Logf("You're using the %s Developer Tool configuration file, the region you're targeting is not declared in this config file. Please check if this is the correct region you're targeting or contact the %s cloud provider for help. If you want to target both OCI regions and %s regions, please set the OCI_ALLOW_ONLY_DEVELOPER_TOOL_CONFIGURATION_REGIONS env var to False.", OciDeveloperToolConfigurationProvider, OciDeveloperToolConfigurationProvider, regionStr)
}
return r
}
// check if short region name provided
if region, ok := shortNameRegion[regionStr]; ok {
r = region
return
}
// check if normal region name provided
potentialRegion := Region(regionStr)
if _, ok := regionRealm[potentialRegion]; ok {
r = potentialRegion
return
}
Debugf("region named: %s, is not recognized from hard-coded region list, will check Region metadata info", stringRegion)
r = checkAndAddRegionMetadata(stringRegion)
return
}
// canStringBeRegion test if the string can be a region, if it can, returns the string as is, otherwise it
// returns an error
var blankRegex = regexp.MustCompile(`\s`)
func canStringBeRegion(stringRegion string) (region string, err error) {
if blankRegex.MatchString(stringRegion) || stringRegion == "" {
return "", fmt.Errorf("region can not be empty or have spaces")
}
return stringRegion, nil
}
// check region info from original map
func checkAndAddRegionMetadata(region string) Region {
switch {
case setRegionMetadataFromCfgFile(&region):
case setRegionMetadataFromEnvVar(&region):
case setRegionFromInstanceMetadataService(&region):
default:
//err := fmt.Errorf("failed to get region metadata information.")
return Region(region)
}
return Region(region)
}
// EnableInstanceMetadataServiceLookup provides the interface to lookup IMDS region info
func EnableInstanceMetadataServiceLookup() {
Debugf("Set visitIMDS 'true' to enable IMDS Lookup.")
visitIMDS = true
}
// setRegionMetadataFromEnvVar checks if region metadata env variable is provided, once it's there, parse and added it
// to region map, and it can make sure the env var can only be visited once.
// Once successfully find the expected region(region name or short code), return true, region name will be stored in
// the input pointer.
func setRegionMetadataFromEnvVar(region *string) bool {
if !readEnvVar {
Debugf("metadata region env variable had already been checked, no need to check again.")
return false //no need to check it again.
}
// Mark readEnvVar Flag as false since it has already been visited.
readEnvVar = false
// check from env variable
if jsonStr, existed := os.LookupEnv(regionMetadataEnvVarName); existed {
Debugf("Raw content of region metadata env var:", jsonStr)
var regionSchema map[string]string
if err := json.Unmarshal([]byte(jsonStr), &regionSchema); err != nil {
Debugf("Can't unmarshal env var, the error info is", err)
return false
}
// check if the specified region is in the env var.
if checkSchemaItems(regionSchema) {
// set mapping table
addRegionSchema(regionSchema)
if regionSchema[regionKeyPropertyName] == *region ||
regionSchema[regionIdentifierPropertyName] == *region {
*region = regionSchema[regionIdentifierPropertyName]
return true
}
}
return false
}
Debugf("The Region Metadata Schema wasn't set in env variable - OCI_REGION_METADATA.")
return false
}
func setRegionMetadataFromCfgFile(region *string) bool {
if setRegionMetadataFromDeveloperToolConfigurationFile(region) {
return true
}
if setRegionMetadataFromRegionCfgFile(region) {
return true
}
return false
}
// setRegionMetadataFromCfgFile checks if region metadata config file is provided, once it's there, parse and add all
// the valid regions to region map, the configuration file can only be visited once.
// Once successfully find the expected region(region name or short code), return true, region name will be stored in
// the input pointer.
func setRegionMetadataFromRegionCfgFile(region *string) bool {
if !readCfgFile {
Debugf("metadata region config file had already been checked, no need to check again.")
return false //no need to check it again.
}
// Mark readCfgFile Flag as false since it has already been visited.
readCfgFile = false
homeFolder := getHomeFolder()
configFile := filepath.Join(homeFolder, regionMetadataCfgDirName, regionMetadataCfgFileName)
if jsonArr, ok := readAndParseConfigFile(&configFile); ok {
added := false
for _, jsonItem := range jsonArr {
if checkSchemaItems(jsonItem) {
addRegionSchema(jsonItem)
if jsonItem[regionKeyPropertyName] == *region ||
jsonItem[regionIdentifierPropertyName] == *region {
*region = jsonItem[regionIdentifierPropertyName]
added = true
}
}
}
return added
}
return false
}
// setRegionMetadataFromDeveloperToolConfigurationFile checks if Developer Tool config file is provided, once it's there, parse and add all
// The default location of the Developer Tool config file is ~/.oci/developer-tool-configuration.json. It will also check the environment variable
// the valid regions to region map, the configuration file can only be visited once.
// Once successfully find the expected region(region name or short code), return true, region name will be stored in
// the input pointer.
func setRegionMetadataFromDeveloperToolConfigurationFile(region *string) bool {
if jsonArr, ok := readAndParseDeveloperToolConfigurationFile(); ok {
added := false
if jsonArr["regions"] == nil {
return false
}
var regionJSON []map[string]string
originalJSONContent, err := json.Marshal(jsonArr["regions"])
if err != nil {
return false
}
err = json.Unmarshal(originalJSONContent, &regionJSON)
if err != nil {
return false
}
if IsEnvVarTrue(OciAllowOnlyDeveloperToolConfigurationRegionsEnvVar) {
resetRegionInfo()
}
for _, jsonItem := range regionJSON {
if checkSchemaItems(jsonItem) {
addRegionSchema(jsonItem)
if jsonItem[regionKeyPropertyName] == *region ||
jsonItem[regionIdentifierPropertyName] == *region {
*region = jsonItem[regionIdentifierPropertyName]
added = true
}
}
}
return added
}
return false
}
func readAndParseConfigFile(configFileName *string) (fileContent []map[string]string, ok bool) {
if content, err := ioutil.ReadFile(*configFileName); err == nil {
Debugf("Raw content of region metadata config file content:", string(content[:]))
if err := json.Unmarshal(content, &fileContent); err != nil {
Debugf("Can't unmarshal config file, the error info is", err)
return
}
ok = true
return
}
Debugf("No Region Metadata Config File provided.")
return
}
func readAndParseDeveloperToolConfigurationFile() (fileContent map[string]interface{}, ok bool) {
homeFolder := getHomeFolder()
configFileName := filepath.Join(homeFolder, regionMetadataCfgDirName, "developer-tool-configuration.json")
if path := os.Getenv(OciDeveloperToolConfigurationFilePathEnvVar); path != "" {
configFileName = path
}
if content, err := ioutil.ReadFile(configFileName); err == nil {
Debugf("Raw content of Developer Tool config file content:", string(content[:]))
if err := json.Unmarshal(content, &fileContent); err != nil {
Debugf("Can't unmarshal env var, the error info is", err)
return
}
ok = true
return
}
Debugf("No Developer Tool Config File provided.")
return
}
func checkDeveloperToolConfigurationFile() bool {
homeFolder := getHomeFolder()
configFileName := filepath.Join(homeFolder, regionMetadataCfgDirName, "developer-tool-configuration.json")
if path := os.Getenv(OciDeveloperToolConfigurationFilePathEnvVar); path != "" {
configFileName = path
}
if _, err := os.Stat(configFileName); err == nil {
return true
}
return false
}
// check map regionRealm's region name, if it's already there, no need to add it.
func addRegionSchema(regionSchema map[string]string) {
r := Region(strings.ToLower(regionSchema[regionIdentifierPropertyName]))
if _, ok := regionRealm[r]; !ok {
// set mapping table
shortNameRegion[regionSchema[regionKeyPropertyName]] = r
realm[regionSchema[realmKeyPropertyName]] = regionSchema[realmDomainComponentPropertyName]
regionRealm[r] = regionSchema[realmKeyPropertyName]
return
}
Debugf("Region {} has already been added, no need to add again.", regionSchema[regionIdentifierPropertyName])
}
// AddRegionSchemaForPlc add region schema to region map
func AddRegionSchemaForPlc(regionSchema map[string]string) {
ociDeveloperToolConfigurationRegionSchemaList = append(ociDeveloperToolConfigurationRegionSchemaList, regionSchema)
addRegionSchema(regionSchema)
// if !IsEnvVarTrue(OciPlcRegionExclusiveEnvVar) {
// addRegionSchema(regionSchema)
// return
// }
// Debugf("Plc region coexist is not enabled, remove exisiting OCI region schema and add PLC region schema.")
// resetRegionInfo()
// bulkAddRegionSchema(ociPlcRegionSchemaList)
}
func resetRegionInfo() {
shortNameRegion = make(map[string]Region)
realm = make(map[string]string)
regionRealm = make(map[Region]string)
}
func bulkAddRegionSchema(regionSchemaList []map[string]string) {
for _, regionSchema := range regionSchemaList {
if checkSchemaItems(regionSchema) {
addRegionSchema(regionSchema)
}
}
}
// check region schema content if all the required contents are provided
func checkSchemaItems(regionSchema map[string]string) bool {
if checkSchemaItem(regionSchema, regionIdentifierPropertyName) &&
checkSchemaItem(regionSchema, realmKeyPropertyName) &&
checkSchemaItem(regionSchema, realmDomainComponentPropertyName) &&
checkSchemaItem(regionSchema, regionKeyPropertyName) {
return true
}
return false
}
// check region schema item is valid, if so, convert it to lower case.
func checkSchemaItem(regionSchema map[string]string, key string) bool {
if val, ok := regionSchema[key]; ok {
if val != "" {
regionSchema[key] = strings.ToLower(val)
return true
}
Debugf("Region metadata schema {} is provided,but content is empty.", key)
return false
}
Debugf("Region metadata schema {} is not provided, please update the content", key)
return false
}
// setRegionFromInstanceMetadataService checks if region metadata can be provided from InstanceMetadataService.
// Once successfully find the expected region(region name or short code), return true, region name will be stored in
// the input pointer.
// setRegionFromInstanceMetadataService will only be checked on the instance, by default it will not be enabled unless
// user explicitly enable it.
func setRegionFromInstanceMetadataService(region *string) bool {
// example of content:
// {
// "realmKey" : "oc1",
// "realmDomainComponent" : "oraclecloud.com",
// "regionKey" : "YUL",
// "regionIdentifier" : "ca-montreal-1"
// }
// Mark visitIMDS Flag as false since it has already been visited.
if !visitIMDS {
Debugf("check from IMDS is disabled or IMDS had already been successfully visited, no need to check again.")
return false
}
content, err := getRegionInfoFromInstanceMetadataService()
if err != nil {
Debugf("Failed to get instance metadata. Error: %v", err)
return false
}
// Mark visitIMDS Flag as false since we have already successfully get the region info from IMDS.
visitIMDS = false
var regionInfo map[string]string
err = json.Unmarshal(content, &regionInfo)
if err != nil {
Debugf("Failed to unmarshal the response content: %v \nError: %v", string(content), err)
return false
}
if checkSchemaItems(regionInfo) {
addRegionSchema(regionInfo)
if regionInfo[regionKeyPropertyName] == *region ||
regionInfo[regionIdentifierPropertyName] == *region {
*region = regionInfo[regionIdentifierPropertyName]
}
} else {
Debugf("Region information is not valid.")
return false
}
return true
}
// getRegionInfoFromInstanceMetadataServiceProd calls instance metadata service and get the region information
func getRegionInfoFromInstanceMetadataServiceProd() ([]byte, error) {
request, _ := http.NewRequest(http.MethodGet, instanceMetadataRegionInfoURLV2, nil)
request.Header.Add("Authorization", "Bearer Oracle")
client := &http.Client{
Timeout: time.Second * 10,
}
resp, err := client.Do(request)
if err != nil {
return nil, fmt.Errorf("failed to call instance metadata service. Error: %v", err)
}
statusCode := resp.StatusCode
defer resp.Body.Close()
content, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to get region information from response body. Error: %v", err)
}
if statusCode != http.StatusOK {
err = fmt.Errorf("HTTP Get failed: URL: %s, Status: %s, Message: %s",
instanceMetadataRegionInfoURLV2, resp.Status, string(content))
return nil, err
}
return content, nil
}
// TemplateParamForPerRealmEndpoint is a template parameter for per-realm endpoint.
type TemplateParamForPerRealmEndpoint struct {
Template string
EndsWithDot bool
}
// SetMissingTemplateParams function will parse the {} template in client host and replace with empty string.
func SetMissingTemplateParams(client *BaseClient) {
templateRegex := regexp.MustCompile(`{.*?}`)
templates := templateRegex.FindAllString(client.Host, -1)
for _, template := range templates {
client.Host = strings.Replace(client.Host, template, "", -1)
}
}
func getOciSdkEnabledServicesMap() map[string]bool {
var enabledMap = make(map[string]bool)
if jsonArr, ok := readAndParseDeveloperToolConfigurationFile(); ok {
if jsonArr["provider"] != nil {
OciDeveloperToolConfigurationProvider = jsonArr["provider"].(string)
}
if jsonArr["allowOnlyDeveloperToolConfigurationRegions"] != nil && jsonArr["allowOnlyDeveloperToolConfigurationRegions"] == false {
ociAllowOnlyDeveloperToolConfigurationRegions = jsonArr["allowOnlyDeveloperToolConfigurationRegions"].(bool)
}
if jsonArr["services"] == nil {
return enabledMap
}
serviesJSON, ok := jsonArr["services"].([]interface{})
if !ok {
return enabledMap
}
re, _ := regexp.Compile(`[^\w]`)
for _, jsonItem := range serviesJSON {
serviceName := strings.ToLower(fmt.Sprint(jsonItem))
serviceName = re.ReplaceAllString(serviceName, "")
enabledMap[serviceName] = true
}
}
return enabledMap
}
// AddServiceToEnabledServicesMap adds the service to the enabledServiceMap
// The service name will auto transit to lower case and remove all the non-word characters.
func AddServiceToEnabledServicesMap(serviceName string) {
if OciSdkEnabledServicesMap == nil {
OciSdkEnabledServicesMap = make(map[string]bool)
}
re, _ := regexp.Compile(`[^\w]`)
serviceName = strings.ToLower(serviceName)
serviceName = re.ReplaceAllString(serviceName, "")
OciSdkEnabledServicesMap[serviceName] = true
}
// CheckForEnabledServices checks if the service is enabled in the enabledServiceMap.
// It will first check if the map is initialized, if not, it will initialize the map.
// If the map is empty, it means all the services are enabled.
// If the map is not empty, it means only the services in the map and value is true are enabled.
func CheckForEnabledServices(serviceName string) bool {
if OciSdkEnabledServicesMap == nil {
OciSdkEnabledServicesMap = getOciSdkEnabledServicesMap()
}
serviceName = strings.ToLower(serviceName)
if len(OciSdkEnabledServicesMap) == 0 {
return true
}
if _, ok := OciSdkEnabledServicesMap[serviceName]; !ok {
return false
}
return OciSdkEnabledServicesMap[serviceName]
}
// CheckAllowOnlyDeveloperToolConfigurationRegions checks if only developer tool configuration regions are allowed
// This function will first check if the OCI_ALLOW_ONLY_DEVELOPER_TOOL_CONFIGURATION_REGIONS environment variable is set.
// If it is set, it will return the value.
// If it is not set, it will return the value from the ociAllowOnlyDeveloperToolConfigurationRegions variable.
func checkAllowOnlyDeveloperToolConfigurationRegions() bool {
if val, ok := os.LookupEnv("OCI_ALLOW_ONLY_DEVELOPER_TOOL_CONFIGURATION_REGIONS"); ok {
return val == "true"
}
return ociAllowOnlyDeveloperToolConfigurationRegions
}

View File

@@ -0,0 +1,820 @@
// Copyright (c) 2016, 2018, 2025, Oracle and/or its affiliates. All rights reserved.
// This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license.
package common
import (
"crypto/rsa"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
)
// AuthenticationType for auth
type AuthenticationType string
const (
// UserPrincipal is default auth type
UserPrincipal AuthenticationType = "user_principal"
// InstancePrincipal is used for instance principal auth type
InstancePrincipal AuthenticationType = "instance_principal"
// InstancePrincipalDelegationToken is used for instance principal delegation token auth type
InstancePrincipalDelegationToken AuthenticationType = "instance_principle_delegation_token"
// ResourcePrincipalDelegationToken is used for resource principal delegation token auth type
ResourcePrincipalDelegationToken AuthenticationType = "resource_principle_delegation_token"
// UnknownAuthenticationType is used for none meaningful auth type
UnknownAuthenticationType AuthenticationType = "unknown_auth_type"
)
// AuthConfig is used for getting auth related paras in config file
type AuthConfig struct {
AuthType AuthenticationType
// IsFromConfigFile is used to point out if the authConfig is from configuration file
IsFromConfigFile bool
OboToken *string
}
// ConfigurationProvider wraps information about the account owner
type ConfigurationProvider interface {
KeyProvider
TenancyOCID() (string, error)
UserOCID() (string, error)
KeyFingerprint() (string, error)
Region() (string, error)
// AuthType() is used for specify the needed auth type, like UserPrincipal, InstancePrincipal, etc.
AuthType() (AuthConfig, error)
}
var fileMutex = sync.Mutex{}
var fileCache = make(map[string][]byte)
// Reads the file contents from cache if present otherwise reads the file.
// If file to be read is frequently updated/refreshed, please use readFile(filename) as readFileFromCache(filename) might return the old contents from the cache.
func readFileFromCache(filename string) ([]byte, error) {
fileMutex.Lock()
defer fileMutex.Unlock()
val, ok := fileCache[filename]
if ok {
return val, nil
}
val, err := ioutil.ReadFile(filename)
if err == nil {
fileCache[filename] = val
}
return val, err
}
// Reads the file and returns the contents
func readFile(filename string) ([]byte, error) {
fileMutex.Lock()
defer fileMutex.Unlock()
val, err := os.ReadFile(filename)
return val, err
}
// IsConfigurationProviderValid Tests all parts of the configuration provider do not return an error, this method will
// not check AuthType(), since authType() is not required to be there.
func IsConfigurationProviderValid(conf ConfigurationProvider) (ok bool, err error) {
baseFn := []func() (string, error){conf.TenancyOCID, conf.UserOCID, conf.KeyFingerprint, conf.Region, conf.KeyID}
for _, fn := range baseFn {
_, err = fn()
ok = err == nil
if err != nil {
return
}
}
_, err = conf.PrivateRSAKey()
ok = err == nil
if err != nil {
return
}
return true, nil
}
// rawConfigurationProvider allows a user to simply construct a configuration provider from raw values.
type rawConfigurationProvider struct {
tenancy string
user string
region string
fingerprint string
privateKey string
privateKeyPassphrase *string
}
// NewRawConfigurationProvider will create a ConfigurationProvider with the arguments of the function
func NewRawConfigurationProvider(tenancy, user, region, fingerprint, privateKey string, privateKeyPassphrase *string) ConfigurationProvider {
return rawConfigurationProvider{tenancy, user, region, fingerprint, privateKey, privateKeyPassphrase}
}
func (p rawConfigurationProvider) PrivateRSAKey() (key *rsa.PrivateKey, err error) {
return PrivateKeyFromBytes([]byte(p.privateKey), p.privateKeyPassphrase)
}
func (p rawConfigurationProvider) KeyID() (keyID string, err error) {
tenancy, err := p.TenancyOCID()
if err != nil {
return
}
user, err := p.UserOCID()
if err != nil {
return
}
fingerprint, err := p.KeyFingerprint()
if err != nil {
return
}
return fmt.Sprintf("%s/%s/%s", tenancy, user, fingerprint), nil
}
func (p rawConfigurationProvider) TenancyOCID() (string, error) {
if p.tenancy == "" {
return "", fmt.Errorf("tenancy OCID can not be empty")
}
return p.tenancy, nil
}
func (p rawConfigurationProvider) UserOCID() (string, error) {
if p.user == "" {
return "", fmt.Errorf("user OCID can not be empty")
}
return p.user, nil
}
func (p rawConfigurationProvider) KeyFingerprint() (string, error) {
if p.fingerprint == "" {
return "", fmt.Errorf("fingerprint can not be empty")
}
return p.fingerprint, nil
}
func (p rawConfigurationProvider) Region() (string, error) {
return canStringBeRegion(p.region)
}
func (p rawConfigurationProvider) AuthType() (AuthConfig, error) {
return AuthConfig{UnknownAuthenticationType, false, nil}, nil
}
// environmentConfigurationProvider reads configuration from environment variables
type environmentConfigurationProvider struct {
PrivateKeyPassword string
EnvironmentVariablePrefix string
}
// ConfigurationProviderEnvironmentVariables creates a ConfigurationProvider from a uniform set of environment variables starting with a prefix
// The env variables should look like: [prefix]_private_key_path, [prefix]_tenancy_ocid, [prefix]_user_ocid, [prefix]_fingerprint
// [prefix]_region
func ConfigurationProviderEnvironmentVariables(environmentVariablePrefix, privateKeyPassword string) ConfigurationProvider {
return environmentConfigurationProvider{EnvironmentVariablePrefix: environmentVariablePrefix,
PrivateKeyPassword: privateKeyPassword}
}
func (p environmentConfigurationProvider) String() string {
return fmt.Sprintf("Configuration provided by environment variables prefixed with: %s", p.EnvironmentVariablePrefix)
}
func (p environmentConfigurationProvider) PrivateRSAKey() (key *rsa.PrivateKey, err error) {
environmentVariable := fmt.Sprintf("%s_%s", p.EnvironmentVariablePrefix, "private_key_path")
var ok bool
var value string
if value, ok = os.LookupEnv(environmentVariable); !ok {
return nil, fmt.Errorf("can not read PrivateKey from env variable: %s", environmentVariable)
}
expandedPath := expandPath(value)
pemFileContent, err := readFileFromCache(expandedPath)
if err != nil {
Debugln("Can not read PrivateKey location from environment variable: " + environmentVariable)
return
}
key, err = PrivateKeyFromBytes(pemFileContent, &p.PrivateKeyPassword)
return
}
func (p environmentConfigurationProvider) KeyID() (keyID string, err error) {
ocid, err := p.TenancyOCID()
if err != nil {
return
}
userocid, err := p.UserOCID()
if err != nil {
return
}
fingerprint, err := p.KeyFingerprint()
if err != nil {
return
}
return fmt.Sprintf("%s/%s/%s", ocid, userocid, fingerprint), nil
}
func (p environmentConfigurationProvider) TenancyOCID() (value string, err error) {
environmentVariable := fmt.Sprintf("%s_%s", p.EnvironmentVariablePrefix, "tenancy_ocid")
var ok bool
if value, ok = os.LookupEnv(environmentVariable); !ok {
err = fmt.Errorf("can not read Tenancy from environment variable %s", environmentVariable)
} else if value == "" {
err = fmt.Errorf("tenancy OCID can not be empty when reading from environmental variable")
}
return
}
func (p environmentConfigurationProvider) UserOCID() (value string, err error) {
environmentVariable := fmt.Sprintf("%s_%s", p.EnvironmentVariablePrefix, "user_ocid")
var ok bool
if value, ok = os.LookupEnv(environmentVariable); !ok {
err = fmt.Errorf("can not read user id from environment variable %s", environmentVariable)
} else if value == "" {
err = fmt.Errorf("user OCID can not be empty when reading from environmental variable")
}
return
}
func (p environmentConfigurationProvider) KeyFingerprint() (value string, err error) {
environmentVariable := fmt.Sprintf("%s_%s", p.EnvironmentVariablePrefix, "fingerprint")
var ok bool
if value, ok = os.LookupEnv(environmentVariable); !ok {
err = fmt.Errorf("can not read fingerprint from environment variable %s", environmentVariable)
} else if value == "" {
err = fmt.Errorf("fingerprint can not be empty when reading from environmental variable")
}
return
}
func (p environmentConfigurationProvider) Region() (value string, err error) {
environmentVariable := fmt.Sprintf("%s_%s", p.EnvironmentVariablePrefix, "region")
var ok bool
if value, ok = os.LookupEnv(environmentVariable); !ok {
err = fmt.Errorf("can not read region from environment variable %s", environmentVariable)
return value, err
}
return canStringBeRegion(value)
}
func (p environmentConfigurationProvider) AuthType() (AuthConfig, error) {
return AuthConfig{UnknownAuthenticationType, false, nil},
fmt.Errorf("unsupported, keep the interface")
}
// fileConfigurationProvider. reads configuration information from a file
type fileConfigurationProvider struct {
//The path to the configuration file
ConfigPath string
//The password for the private key
PrivateKeyPassword string
//The profile for the configuration
Profile string
//ConfigFileInfo
FileInfo *configFileInfo
//Mutex to protect the config file
configMux sync.Mutex
}
type fileConfigurationProviderError struct {
err error
}
func (fpe fileConfigurationProviderError) Error() string {
return fmt.Sprintf("%s\nFor more info about config file and how to get required information, see https://docs.oracle.com/en-us/iaas/Content/API/Concepts/sdkconfig.htm", fpe.err)
}
// ConfigurationProviderFromFile creates a configuration provider from a configuration file
// by reading the "DEFAULT" profile
func ConfigurationProviderFromFile(configFilePath, privateKeyPassword string) (ConfigurationProvider, error) {
if configFilePath == "" {
return nil, fmt.Errorf("config file path can not be empty")
}
return fileConfigurationProvider{
ConfigPath: configFilePath,
PrivateKeyPassword: privateKeyPassword,
Profile: "DEFAULT",
configMux: sync.Mutex{}}, nil
}
// ConfigurationProviderFromFileWithProfile creates a configuration provider from a configuration file
// and the given profile
func ConfigurationProviderFromFileWithProfile(configFilePath, profile, privateKeyPassword string) (ConfigurationProvider, error) {
if configFilePath == "" {
return nil, fileConfigurationProviderError{err: fmt.Errorf("config file path can not be empty")}
}
return fileConfigurationProvider{
ConfigPath: configFilePath,
PrivateKeyPassword: privateKeyPassword,
Profile: profile,
configMux: sync.Mutex{}}, nil
}
type configFileInfo struct {
UserOcid, Fingerprint, KeyFilePath, TenancyOcid, Region, Passphrase, SecurityTokenFilePath, DelegationTokenFilePath,
AuthenticationType string
PresentConfiguration rune
}
const (
hasTenancy = 1 << iota
hasUser
hasFingerprint
hasRegion
hasKeyFile
hasPassphrase
hasSecurityTokenFile
hasDelegationTokenFile
hasAuthenticationType
none
)
var profileRegex = regexp.MustCompile(`^\[(.*)\]`)
func parseConfigFile(data []byte, profile string) (info *configFileInfo, err error) {
if len(data) == 0 {
return nil, fileConfigurationProviderError{err: fmt.Errorf("configuration file content is empty")}
}
content := string(data)
splitContent := strings.Split(content, "\n")
//Look for profile
for i, line := range splitContent {
if match := profileRegex.FindStringSubmatch(line); len(match) > 1 && match[1] == profile {
start := i + 1
return parseConfigAtLine(start, splitContent)
}
}
return nil, fileConfigurationProviderError{err: fmt.Errorf("configuration file did not contain profile: %s", profile)}
}
func parseConfigAtLine(start int, content []string) (info *configFileInfo, err error) {
var configurationPresent rune
info = &configFileInfo{}
for i := start; i < len(content); i++ {
line := content[i]
if profileRegex.MatchString(line) {
break
}
if !strings.Contains(line, "=") {
continue
}
splits := strings.Split(line, "=")
switch key, value := strings.TrimSpace(splits[0]), strings.TrimSpace(splits[1]); strings.ToLower(key) {
case "passphrase", "pass_phrase":
configurationPresent = configurationPresent | hasPassphrase
info.Passphrase = value
case "user":
configurationPresent = configurationPresent | hasUser
info.UserOcid = value
case "fingerprint":
configurationPresent = configurationPresent | hasFingerprint
info.Fingerprint = value
case "key_file":
configurationPresent = configurationPresent | hasKeyFile
info.KeyFilePath = value
case "tenancy":
configurationPresent = configurationPresent | hasTenancy
info.TenancyOcid = value
case "region":
configurationPresent = configurationPresent | hasRegion
info.Region = value
case "security_token_file":
configurationPresent = configurationPresent | hasSecurityTokenFile
info.SecurityTokenFilePath = value
case "delegation_token_file":
configurationPresent = configurationPresent | hasDelegationTokenFile
info.DelegationTokenFilePath = value
case "authentication_type":
configurationPresent = configurationPresent | hasAuthenticationType
info.AuthenticationType = value
}
}
info.PresentConfiguration = configurationPresent
return
}
// cleans and expands the path if it contains a tilde , returns the expanded path or the input path as is if not expansion
// was performed
func expandPath(filename string) (expandedPath string) {
cleanedPath := filepath.Clean(filename)
expandedPath = cleanedPath
if strings.HasPrefix(cleanedPath, "~") {
rest := cleanedPath[2:]
expandedPath = filepath.Join(getHomeFolder(), rest)
}
return
}
func openConfigFile(configFilePath string) (data []byte, err error) {
expandedPath := expandPath(configFilePath)
data, err = readFileFromCache(expandedPath)
if err != nil {
err = fmt.Errorf("can not read config file: %s due to: %s", configFilePath, err.Error())
}
return
}
func (p fileConfigurationProvider) String() string {
return fmt.Sprintf("Configuration provided by file: %s", p.ConfigPath)
}
func (p fileConfigurationProvider) readAndParseConfigFile() (info *configFileInfo, err error) {
p.configMux.Lock()
defer p.configMux.Unlock()
if p.FileInfo != nil {
return p.FileInfo, nil
}
if p.ConfigPath == "" {
return nil, fileConfigurationProviderError{err: fmt.Errorf("configuration path can not be empty")}
}
data, err := openConfigFile(p.ConfigPath)
if err != nil {
err = fileConfigurationProviderError{err: fmt.Errorf("error while parsing config file: %s. Due to: %s", p.ConfigPath, err.Error())}
return
}
p.FileInfo, err = parseConfigFile(data, p.Profile)
return p.FileInfo, err
}
func presentOrError(value string, expectedConf, presentConf rune, confMissing string) (string, error) {
if presentConf&expectedConf == expectedConf {
return value, nil
}
return "", fileConfigurationProviderError{err: errors.New(confMissing + " configuration is missing from file")}
}
func (p fileConfigurationProvider) TenancyOCID() (value string, err error) {
info, err := p.readAndParseConfigFile()
if err != nil {
err = fileConfigurationProviderError{err: fmt.Errorf("can not read tenancy configuration due to: %s", err.Error())}
return
}
value, err = presentOrError(info.TenancyOcid, hasTenancy, info.PresentConfiguration, "tenancy")
if err == nil && value == "" {
err = fileConfigurationProviderError{err: fmt.Errorf("tenancy OCID can not be empty when reading from config file")}
}
return
}
func (p fileConfigurationProvider) UserOCID() (value string, err error) {
info, err := p.readAndParseConfigFile()
if err != nil {
err = fileConfigurationProviderError{err: fmt.Errorf("can not read tenancy configuration due to: %s", err.Error())}
return
}
if value, err = presentOrError(info.UserOcid, hasUser, info.PresentConfiguration, "user"); err != nil {
// need to check if securityTokenPath is provided, if security token is provided, userOCID can be "".
if _, stErr := presentOrError(info.SecurityTokenFilePath, hasSecurityTokenFile, info.PresentConfiguration,
"securityTokenPath"); stErr == nil {
err = nil
}
}
return
}
func (p fileConfigurationProvider) KeyFingerprint() (value string, err error) {
info, err := p.readAndParseConfigFile()
if err != nil {
err = fileConfigurationProviderError{err: fmt.Errorf("can not read tenancy configuration due to: %s", err.Error())}
return
}
value, err = presentOrError(info.Fingerprint, hasFingerprint, info.PresentConfiguration, "fingerprint")
if err == nil && value == "" {
return "", fmt.Errorf("fingerprint can not be empty when reading from config file")
}
return
}
func (p fileConfigurationProvider) KeyID() (keyID string, err error) {
tenancy, err := p.TenancyOCID()
if err != nil {
return
}
fingerprint, err := p.KeyFingerprint()
if err != nil {
return
}
info, err := p.readAndParseConfigFile()
if err != nil {
err = fileConfigurationProviderError{err: fmt.Errorf("can not read tenancy configuration due to: %s", err.Error())}
return
}
if info.PresentConfiguration&hasUser == hasUser {
if info.UserOcid == "" {
err = fileConfigurationProviderError{err: fmt.Errorf("user cannot be empty in the config file")}
return
}
return fmt.Sprintf("%s/%s/%s", tenancy, info.UserOcid, fingerprint), nil
}
filePath, pathErr := presentOrError(info.SecurityTokenFilePath, hasSecurityTokenFile, info.PresentConfiguration, "securityTokenFilePath")
if pathErr == nil {
rawString, err := getTokenContent(filePath)
if err != nil {
return "", fileConfigurationProviderError{err: err}
}
return "ST$" + rawString, nil
}
err = fileConfigurationProviderError{err: fmt.Errorf("can not read SecurityTokenFilePath from configuration file due to: %s", pathErr.Error())}
return
}
func (p fileConfigurationProvider) PrivateRSAKey() (key *rsa.PrivateKey, err error) {
info, err := p.readAndParseConfigFile()
if err != nil {
err = fileConfigurationProviderError{err: fmt.Errorf("can not read tenancy configuration due to: %s", err.Error())}
return
}
filePath, err := presentOrError(info.KeyFilePath, hasKeyFile, info.PresentConfiguration, "key file path")
if err != nil {
return
}
expandedPath := expandPath(filePath)
pemFileContent, err := readFileFromCache(expandedPath)
if err != nil {
err = fileConfigurationProviderError{err: fmt.Errorf("can not read PrivateKey from configuration file due to: %s", err.Error())}
return
}
password := p.PrivateKeyPassword
if password == "" && ((info.PresentConfiguration & hasPassphrase) == hasPassphrase) {
password = info.Passphrase
}
key, err = PrivateKeyFromBytes(pemFileContent, &password)
return
}
func (p fileConfigurationProvider) Region() (value string, err error) {
info, err := p.readAndParseConfigFile()
if err != nil {
err = fileConfigurationProviderError{err: fmt.Errorf("can not read region configuration due to: %s", err.Error())}
return
}
value, err = presentOrError(info.Region, hasRegion, info.PresentConfiguration, "region")
if err != nil {
val, error := getRegionFromEnvVar()
if error != nil {
err = fileConfigurationProviderError{err: fmt.Errorf("region configuration is missing from file, nor for OCI_REGION env var")}
return
}
value = val
}
return canStringBeRegion(value)
}
func (p fileConfigurationProvider) AuthType() (AuthConfig, error) {
info, err := p.readAndParseConfigFile()
if err != nil {
err = fmt.Errorf("can not read tenancy configuration due to: %s", err.Error())
return AuthConfig{UnknownAuthenticationType, true, nil}, err
}
val, _ := presentOrError(info.AuthenticationType, hasAuthenticationType, info.PresentConfiguration, "authentication_type")
if val == "instance_principal" {
if filePath, err := presentOrError(info.DelegationTokenFilePath, hasDelegationTokenFile, info.PresentConfiguration, "delegationTokenFilePath"); err == nil {
if delegationToken, err := getTokenContent(filePath); err == nil && delegationToken != "" {
Debugf("delegation token content is %s, and error is %s ", delegationToken, err)
return AuthConfig{InstancePrincipalDelegationToken, true, &delegationToken}, nil
}
return AuthConfig{UnknownAuthenticationType, true, nil}, err
}
// normal instance principle
return AuthConfig{InstancePrincipal, true, nil}, nil
}
// by default, if no "authentication_type" is provided, just treated as user principle type, and will not return error
return AuthConfig{UserPrincipal, true, nil}, nil
}
func getTokenContent(filePath string) (string, error) {
expandedPath := expandPath(filePath)
tokenFileContent, err := readFile(expandedPath)
if err != nil {
err = fileConfigurationProviderError{err: fmt.Errorf("can not read token content from configuration file due to: %s", err.Error())}
return "", err
}
return string(tokenFileContent), nil
}
// A configuration provider that look for information in multiple configuration providers
type composingConfigurationProvider struct {
Providers []ConfigurationProvider
}
// ComposingConfigurationProvider creates a composing configuration provider with the given slice of configuration providers
// A composing provider will return the configuration of the first provider that has the required property
// if no provider has the property it will return an error.
func ComposingConfigurationProvider(providers []ConfigurationProvider) (ConfigurationProvider, error) {
if len(providers) == 0 {
return nil, fmt.Errorf("providers can not be an empty slice")
}
for i, p := range providers {
if p == nil {
return nil, fmt.Errorf("provider in position: %d is nil. ComposingConfiurationProvider does not support nil values", i)
}
}
return composingConfigurationProvider{Providers: providers}, nil
}
func (c composingConfigurationProvider) TenancyOCID() (string, error) {
for _, p := range c.Providers {
val, err := p.TenancyOCID()
if err == nil {
return val, nil
}
Debugf("did not find a proper configuration for tenancy, err: %v", err)
}
return "", fmt.Errorf("did not find a proper configuration for tenancy")
}
func (c composingConfigurationProvider) UserOCID() (string, error) {
for _, p := range c.Providers {
val, err := p.UserOCID()
if err == nil {
return val, nil
}
Debugf("did not find a proper configuration for keyFingerprint, err: %v", err)
}
return "", fmt.Errorf("did not find a proper configuration for user")
}
func (c composingConfigurationProvider) KeyFingerprint() (string, error) {
for _, p := range c.Providers {
val, err := p.KeyFingerprint()
if err == nil {
return val, nil
}
}
return "", fmt.Errorf("did not find a proper configuration for keyFingerprint")
}
func (c composingConfigurationProvider) Region() (string, error) {
for _, p := range c.Providers {
val, err := p.Region()
if err == nil {
return val, nil
}
}
if val, err := getRegionFromEnvVar(); err == nil {
return val, nil
}
return "", fmt.Errorf("did not find a proper configuration for region, nor for OCI_REGION env var")
}
func (c composingConfigurationProvider) KeyID() (string, error) {
for _, p := range c.Providers {
val, err := p.KeyID()
if err == nil {
return val, nil
}
}
return "", fmt.Errorf("did not find a proper configuration for key id")
}
func (c composingConfigurationProvider) PrivateRSAKey() (*rsa.PrivateKey, error) {
for _, p := range c.Providers {
val, err := p.PrivateRSAKey()
if err == nil {
return val, nil
}
}
return nil, fmt.Errorf("did not find a proper configuration for private key")
}
func (c composingConfigurationProvider) AuthType() (AuthConfig, error) {
// only check the first default fileConfigProvider
authConfig, err := c.Providers[0].AuthType()
if err == nil && authConfig.AuthType != UnknownAuthenticationType {
return authConfig, nil
}
return AuthConfig{UnknownAuthenticationType, false, nil}, fmt.Errorf("did not find a proper configuration for auth type")
}
func getRegionFromEnvVar() (string, error) {
regionEnvVar := "OCI_REGION"
if region, existed := os.LookupEnv(regionEnvVar); existed {
return region, nil
}
return "", fmt.Errorf("did not find OCI_REGION env var")
}
type sessionTokenConfigurationProvider struct {
fileConfigurationProvider
}
func (p sessionTokenConfigurationProvider) UserOCID() (value string, err error) {
info, err := p.readAndParseConfigFile()
if err != nil {
err = fileConfigurationProviderError{err: fmt.Errorf("can not read the configuration due to: %s", err.Error())}
return
}
// In case of session token-based authentication, userOCID will not be present
// need to check if session token path is provided in the configuration
if _, stErr := presentOrError(info.SecurityTokenFilePath, hasSecurityTokenFile, info.PresentConfiguration,
"securityTokenPath"); stErr == nil {
err = nil
}
return
}
func (p sessionTokenConfigurationProvider) KeyID() (keyID string, err error) {
_, err = p.TenancyOCID()
if err != nil {
return
}
_, err = p.KeyFingerprint()
if err != nil {
return
}
info, err := p.readAndParseConfigFile()
if err != nil {
err = fileConfigurationProviderError{err: fmt.Errorf("can not read SessionTokenFilePath configuration due to: %s", err.Error())}
return
}
filePath, pathErr := presentOrError(info.SecurityTokenFilePath, hasSecurityTokenFile, info.PresentConfiguration, "securityTokenFilePath")
if pathErr == nil {
rawString, err := getTokenContent(filePath)
if err != nil {
return "", fileConfigurationProviderError{err: err}
}
return "ST$" + rawString, nil
}
err = fileConfigurationProviderError{err: fmt.Errorf("can not read SessionTokenFilePath from configuration file due to: %s", pathErr.Error())}
return
}
// ConfigurationProviderForSessionToken creates a session token configuration provider from a configuration file
// by reading the "DEFAULT" profile
func ConfigurationProviderForSessionToken(configFilePath, privateKeyPassword string) (ConfigurationProvider, error) {
if configFilePath == "" {
return nil, fileConfigurationProviderError{err: fmt.Errorf("config file path can not be empty")}
}
return sessionTokenConfigurationProvider{
fileConfigurationProvider{
ConfigPath: configFilePath,
PrivateKeyPassword: privateKeyPassword,
Profile: "DEFAULT",
configMux: sync.Mutex{}}}, nil
}
// ConfigurationProviderForSessionTokenWithProfile creates a session token configuration provider from a configuration file
// by reading the given profile
func ConfigurationProviderForSessionTokenWithProfile(configFilePath, profile, privateKeyPassword string) (ConfigurationProvider, error) {
if configFilePath == "" {
return nil, fileConfigurationProviderError{err: fmt.Errorf("config file path can not be empty")}
}
return sessionTokenConfigurationProvider{
fileConfigurationProvider{
ConfigPath: configFilePath,
PrivateKeyPassword: privateKeyPassword,
Profile: profile,
configMux: sync.Mutex{}}}, nil
}
func (p sessionTokenConfigurationProvider) Refreshable() bool {
return true
}
// RefreshableConfigurationProvider the interface to identity if the config provider is refreshable
type RefreshableConfigurationProvider interface {
Refreshable() bool
}

View File

@@ -0,0 +1,306 @@
// Copyright (c) 2016, 2018, 2025, Oracle and/or its affiliates. All rights reserved.
// This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license.
package common
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"strings"
"syscall"
"github.com/sony/gobreaker"
)
// ServiceError models all potential errors generated the service call
type ServiceError interface {
// The http status code of the error
GetHTTPStatusCode() int
// The human-readable error string as sent by the service
GetMessage() string
// A short error code that defines the error, meant for programmatic parsing.
// See https://docs.oracle.com/iaas/Content/API/References/apierrors.htm
GetCode() string
// Unique Oracle-assigned identifier for the request.
// If you need to contact Oracle about a particular request, please provide the request ID.
GetOpcRequestID() string
}
// ServiceErrorRichInfo models all potential errors generated the service call and contains rich info for debugging purpose
type ServiceErrorRichInfo interface {
ServiceError
// The service this service call is sending to
GetTargetService() string
// The API name this service call is sending to
GetOperationName() string
// The timestamp when this request is made
GetTimestamp() SDKTime
// The endpoint and the Http method of this service call
GetRequestTarget() string
// The client version, in this case the oci go sdk version
GetClientVersion() string
// The API reference doc link for this API, optional and maybe empty
GetOperationReferenceLink() string
// Troubleshooting doc link
GetErrorTroubleshootingLink() string
}
// ServiceErrorLocalizationMessage models all potential errors generated the service call and has localized error message info
type ServiceErrorLocalizationMessage interface {
ServiceErrorRichInfo
// The original error message string as sent by the service
GetOriginalMessage() string
// The values to be substituted into the originalMessageTemplate, expressed as a string-to-string map.
GetMessageArgument() map[string]string
// Template in ICU MessageFormat for the human-readable error string in English, but without the values replaced
GetOriginalMessageTemplate() string
}
type servicefailure struct {
StatusCode int
Code string `json:"code,omitempty"`
Message string `json:"message,omitempty"`
OriginalMessage string `json:"originalMessage"`
OriginalMessageTemplate string `json:"originalMessageTemplate"`
MessageArgument map[string]string `json:"messageArguments"`
OpcRequestID string `json:"opc-request-id"`
// debugging information
TargetService string `json:"target-service"`
OperationName string `json:"operation-name"`
Timestamp SDKTime `json:"timestamp"`
RequestTarget string `json:"request-target"`
ClientVersion string `json:"client-version"`
// troubleshooting guidance
OperationReferenceLink string `json:"operation-reference-link"`
ErrorTroubleshootingLink string `json:"error-troubleshooting-link"`
}
func newServiceFailureFromResponse(response *http.Response) error {
var err error
var timestamp SDKTime
t, err := tryParsingTimeWithValidFormatsForHeaders([]byte(response.Header.Get("Date")), "Date")
if err != nil {
timestamp = *now()
} else {
timestamp = sdkTimeFromTime(t)
}
se := servicefailure{
StatusCode: response.StatusCode,
Code: "BadErrorResponse",
OpcRequestID: response.Header.Get("opc-request-id"),
Timestamp: timestamp,
ClientVersion: defaultSDKMarker + "/" + Version(),
RequestTarget: fmt.Sprintf("%s %s", response.Request.Method, response.Request.URL),
}
//If there is an error consume the body, entirely
body, err := ioutil.ReadAll(response.Body)
if err != nil {
se.Message = fmt.Sprintf("The body of the response was not readable, due to :%s", err.Error())
return se
}
err = json.Unmarshal(body, &se)
if err != nil {
Debugf("Error response could not be parsed due to: %s", err.Error())
se.Message = fmt.Sprintf("Failed to parse json from response body due to: %s. With response body %s.", err.Error(), string(body[:]))
return se
}
return se
}
// PostProcessServiceError process the service error after an error is raised and complete it with extra information
func PostProcessServiceError(err error, service string, method string, apiReferenceLink string) error {
var serviceFailure servicefailure
if _, ok := err.(servicefailure); !ok {
return err
}
serviceFailure = err.(servicefailure)
serviceFailure.OperationName = method
serviceFailure.TargetService = service
serviceFailure.ErrorTroubleshootingLink = fmt.Sprintf("https://docs.oracle.com/iaas/Content/API/References/apierrors.htm#apierrors_%v__%v_%s", serviceFailure.StatusCode, serviceFailure.StatusCode, strings.ToLower(serviceFailure.Code))
serviceFailure.OperationReferenceLink = apiReferenceLink
return serviceFailure
}
func (se servicefailure) Error() string {
return fmt.Sprintf(`Error returned by %s Service. Http Status Code: %d. Error Code: %s. Opc request id: %s. Message: %s
Operation Name: %s
Timestamp: %s
Client Version: %s
Request Endpoint: %s
Troubleshooting Tips: See %s for more information about resolving this error.%s
To get more info on the failing request, you can set OCI_GO_SDK_DEBUG env var to info or higher level to log the request/response details.
If you are unable to resolve this %s issue, please contact Oracle support and provide them this full error message.`,
se.TargetService, se.StatusCode, se.Code, se.OpcRequestID, se.Message, se.OperationName, se.Timestamp, se.ClientVersion, se.RequestTarget, se.ErrorTroubleshootingLink, se.getOperationReferenceMessage(), se.TargetService)
}
func (se servicefailure) getOperationReferenceMessage() string {
if se.OperationReferenceLink == "" {
return ""
}
return fmt.Sprintf("\nAlso see %s for details on this operation's requirements.", se.OperationReferenceLink)
}
func (se servicefailure) GetHTTPStatusCode() int {
return se.StatusCode
}
func (se servicefailure) GetMessage() string {
return se.Message
}
func (se servicefailure) GetOriginalMessage() string {
return se.OriginalMessage
}
func (se servicefailure) GetOriginalMessageTemplate() string {
return se.OriginalMessageTemplate
}
func (se servicefailure) GetMessageArgument() map[string]string {
return se.MessageArgument
}
func (se servicefailure) GetCode() string {
return se.Code
}
func (se servicefailure) GetOpcRequestID() string {
return se.OpcRequestID
}
func (se servicefailure) GetTargetService() string {
return se.TargetService
}
func (se servicefailure) GetOperationName() string {
return se.OperationName
}
func (se servicefailure) GetTimestamp() SDKTime {
return se.Timestamp
}
func (se servicefailure) GetRequestTarget() string {
return se.RequestTarget
}
func (se servicefailure) GetClientVersion() string {
return se.ClientVersion
}
func (se servicefailure) GetOperationReferenceLink() string {
return se.OperationReferenceLink
}
func (se servicefailure) GetErrorTroubleshootingLink() string {
return se.ErrorTroubleshootingLink
}
// IsServiceError returns false if the error is not service side, otherwise true
// additionally it returns an interface representing the ServiceError
func IsServiceError(err error) (failure ServiceError, ok bool) {
failure, ok = err.(ServiceError)
return
}
// IsServiceErrorRichInfo returns false if the error is not service side or is not containing rich info, otherwise true
// additionally it returns an interface representing the ServiceErrorRichInfo
func IsServiceErrorRichInfo(err error) (failure ServiceErrorRichInfo, ok bool) {
failure, ok = err.(ServiceErrorRichInfo)
return
}
// IsServiceErrorLocalizationMessage returns false if the error is not service side, otherwise true
// additionally it returns an interface representing the ServiceErrorOriginalMessage
func IsServiceErrorLocalizationMessage(err error) (failure ServiceErrorLocalizationMessage, ok bool) {
failure, ok = err.(ServiceErrorLocalizationMessage)
return
}
type deadlineExceededByBackoffError struct{}
func (deadlineExceededByBackoffError) Error() string {
return "now() + computed backoff duration exceeds request deadline"
}
// DeadlineExceededByBackoff is the error returned by Call() when GetNextDuration() returns a time.Duration that would
// force the user to wait past the request deadline before re-issuing a request. This enables us to exit early, since
// we cannot succeed based on the configured retry policy.
var DeadlineExceededByBackoff error = deadlineExceededByBackoffError{}
// NonSeekableRequestRetryFailure is the error returned when the request is with binary request body, and is configured
// retry, but the request body is not retryable
type NonSeekableRequestRetryFailure struct {
err error
}
func (ne NonSeekableRequestRetryFailure) Error() string {
if ne.err == nil {
return "Unable to perform Retry on this request body type, which did not implement seek() interface"
}
return fmt.Sprintf("%s. Unable to perform Retry on this request body type, which did not implement seek() interface", ne.err.Error())
}
// IsNetworkError validates if an error is a net.Error and check if it's temporary or timeout
func IsNetworkError(err error) bool {
if err == nil {
return false
}
if errors.Is(err, syscall.ECONNRESET) {
return true
}
if r, ok := err.(net.Error); ok && (r.Timeout() || strings.Contains(err.Error(), "net/http: HTTP/1.x transport connection broken")) {
return true
}
return false
}
// IsCircuitBreakerError validates if an error's text is Open state ErrOpenState or HalfOpen state ErrTooManyRequests
func IsCircuitBreakerError(err error) bool {
if err == nil {
return false
}
if err.Error() == gobreaker.ErrOpenState.Error() || err.Error() == gobreaker.ErrTooManyRequests.Error() {
return true
}
return false
}
func getCircuitBreakerError(request *http.Request, err error, cbr *OciCircuitBreaker) error {
cbErr := fmt.Errorf("%s, so this request was not sent to the %s service.\n\n The circuit breaker was opened because the %s service failed too many times recently. "+
"Because the circuit breaker has been opened, requests within a %.2f second window of when the circuit breaker opened will not be sent to the %s service.\n\n"+
"URL which circuit breaker prevented request to - %s \n Circuit Breaker Info \n Name - %s \n State - %s \n\n Errors from %s service which opened the circuit breaker:\n\n%s",
err, cbr.Cbst.serviceName, cbr.Cbst.serviceName, cbr.Cbst.openStateWindow.Seconds(), cbr.Cbst.serviceName, request.URL.Host+request.URL.Path, cbr.Cbst.name, cbr.Cb.State().String(), cbr.Cbst.serviceName, cbr.GetHistory())
return cbErr
}
// StatErrCode is a type which wraps error's statusCode and errorCode from service end
type StatErrCode struct {
statusCode int
errorCode string
}

View File

@@ -0,0 +1,467 @@
// Copyright (c) 2016, 2018, 2025, Oracle and/or its affiliates. All rights reserved.
// This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license.
package common
import (
"bytes"
"errors"
"fmt"
"os"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/gofrs/flock"
)
const (
// OciGoSdkEcConfigEnvVarName contains the name of the environment variable that can be used to configure the eventual consistency (EC) communication mode.
// Allowed values for environment variable:
// 1. OCI_GO_SDK_EC_CONFIG = "file,/path/to/shared/timestamp/file"
// 2. OCI_GO_SDK_EC_CONFIG = "inprocess"
// 3. absent -- same as OCI_GO_SDK_EC_CONFIG = "inprocess"
OciGoSdkEcConfigEnvVarName string = "OCI_GO_SDK_EC_CONFIG"
)
//
// Eventual consistency communication mode
//
// EcMode is the eventual consistency (EC) communication mode used.
type EcMode int64
const (
// Uninitialized means the EC communication mode has not been set yet.
Uninitialized EcMode = iota // 0
// InProcess is the default EC communication mode which only communicates the end-of-window timestamp inside the same process.
InProcess
// File is the EC communication mode that uses a file to communicate the end-of-window timestamp using a file visible across processes.
// Locking is performed using a lock file.
File
)
var (
affectedByEventualConsistencyRetryStatusCodeMap = map[StatErrCode]bool{
{400, "RelatedResourceNotAuthorizedOrNotFound"}: true,
{404, "NotAuthorizedOrNotFound"}: true,
{409, "NotAuthorizedOrResourceAlreadyExists"}: true,
{409, "ResourceAlreadyExists"}: true,
{400, "InsufficientServicePermissions"}: true,
{400, "ResourceDisabled"}: true,
}
)
// IsErrorAffectedByEventualConsistency returns true if the error is affected by eventual consistency.
func IsErrorAffectedByEventualConsistency(Error error) bool {
if err, ok := IsServiceError(Error); ok {
return affectedByEventualConsistencyRetryStatusCodeMap[StatErrCode{err.GetHTTPStatusCode(), err.GetCode()}]
}
return false
}
func getEcMode(mode string) EcMode {
var lmode = strings.ToLower(mode)
switch lmode {
case "file":
return File
case "inprocess":
return InProcess
}
ecLogf("%s: Unknown ec mode '%s', assuming 'inprocess'", OciGoSdkEcConfigEnvVarName, mode)
return InProcess
}
// EventuallyConsistentContext contains the information about the end of the eventually consistent window.
type EventuallyConsistentContext struct {
// memory-based
endOfWindow atomic.Value
lock sync.RWMutex
timeNowProvider func() time.Time
// mode selector
ecMode EcMode
// file-based
// timestampFileName and timestampLockFile should be set to files that
// are accessible by all processes that need to share information about
// eventual consistency.
// A sensible choice are files inside the temp directory, as returned by os.TempDir()
timestampFileName *string
timestampFileLock *flock.Flock
// lock and unlock functions
readLock func(e *EventuallyConsistentContext) error
readUnlock func(e *EventuallyConsistentContext) error
writeLock func(e *EventuallyConsistentContext) error
writeUnlock func(e *EventuallyConsistentContext) error
// get/set functions
getEndOfWindowUnsynchronized func(e *EventuallyConsistentContext) (*time.Time, error)
setEndOfWindowUnsynchronized func(e *EventuallyConsistentContext, newEndOfWindowTime *time.Time) error
}
// newEcContext creates a new EC context based on the OCI_GO_SDK_EC_CONFIG environment variable.
func newEcContext() *EventuallyConsistentContext {
ecConfig, ecConfigProvided := os.LookupEnv(OciGoSdkEcConfigEnvVarName)
if !ecConfigProvided {
ecConfig = ""
}
commaIndex := strings.Index(ecConfig, ",")
var ecConfigMode = ecConfig
var ecConfigRest = ""
if commaIndex >= 0 {
ecConfigMode = ecConfig[:commaIndex]
ecConfigRest = ecConfig[commaIndex+1:]
}
ecMode := getEcMode(ecConfigMode)
switch ecMode {
case File:
if len(ecConfigRest) < 1 {
ecLogf("%s: Expected file name after comma for 'File' mode ('file,/path/to/file'), was: '%s'", OciGoSdkEcConfigEnvVarName, ecConfig)
return nil
}
return newEcContextFile(ecConfigRest)
}
return newEcContextInProcess()
}
// newEcContextInProcess creates a new in-process EC context.
func newEcContextInProcess() *EventuallyConsistentContext {
ecContext := EventuallyConsistentContext{
ecMode: InProcess,
readLock: ecInProcessReadLock,
readUnlock: ecInProcessReadUnlock,
writeLock: ecInProcessWriteLock,
writeUnlock: ecInProcessWriteUnlock,
getEndOfWindowUnsynchronized: ecInProcessGetEndOfWindowUnsynchronized,
setEndOfWindowUnsynchronized: ecInProcessSetEndOfWindowUnsynchronized,
timeNowProvider: func() time.Time { return time.Now() },
}
return &ecContext
}
// newEcContextFile creates a new EC context kept in a file.
// timestampFileName should be set to a file accessible by all processes that
// need to share information about eventual consistency.
// A sensible choice are files inside the temp directory, as returned by os.TempDir()
// The lock file will use the same name, with the suffix ".lock" added.
func newEcContextFile(timestampFileName string) *EventuallyConsistentContext {
timestampLockFileName := timestampFileName + ".lock"
ecContext := EventuallyConsistentContext{
ecMode: File,
readLock: ecFileReadLock,
readUnlock: ecFileReadUnlock,
writeLock: ecFileWriteLock,
writeUnlock: ecFileWriteUnlock,
getEndOfWindowUnsynchronized: ecFileGetEndOfWindowUnsynchronized,
setEndOfWindowUnsynchronized: ecFileSetEndOfWindowUnsynchronized,
timeNowProvider: func() time.Time { return time.Now() },
timestampFileName: &timestampFileName,
timestampFileLock: flock.New(timestampLockFileName),
}
ecDebugf("%s: Using file modification time of file '%s' and lock file '%s'", OciGoSdkEcConfigEnvVarName, *ecContext.timestampFileName, timestampLockFileName)
return &ecContext
}
// InitializeEcContextFromEnvVar initializes the EcContext variable as configured
// in the OCI_GO_SDK_EC_CONFIG environment variable.
func InitializeEcContextFromEnvVar() {
EcContext = newEcContext()
}
// InitializeEcContextInProcess initializes the EcContext variable to be in-process only.
func InitializeEcContextInProcess() {
EcContext = newEcContextInProcess()
}
// InitializeEcContextFile initializes the EcContext variable to be kept in a timestamp file,
// protected by a lock file.
// timestampFileName should be set to a file accessible by all processes that
// need to share information about eventual consistency.
// A sensible choice are files inside the temp directory, as returned by os.TempDir()
// The lock file will use the same name, with the suffix ".lock" added.
func InitializeEcContextFile(timestampFileName string) {
EcContext = newEcContextFile(timestampFileName)
}
//
// InProcess functions
//
func ecInProcessReadLock(e *EventuallyConsistentContext) error {
e.lock.RLock()
return nil
}
func ecInProcessReadUnlock(e *EventuallyConsistentContext) error {
e.lock.RUnlock()
return nil
}
func ecInProcessWriteLock(e *EventuallyConsistentContext) error {
e.lock.Lock()
return nil
}
func ecInProcessWriteUnlock(e *EventuallyConsistentContext) error {
e.lock.Unlock()
return nil
}
// ecInProcessGetEndOfWindowUnsynchronized returns the end time of an eventually consistent window,
// or nil if no eventually consistent requests were made.
// There is no mutex synchronization.
func ecInProcessGetEndOfWindowUnsynchronized(e *EventuallyConsistentContext) (*time.Time, error) {
untyped := e.endOfWindow.Load() // returns nil if there has been no call to Store for this Value
if untyped == nil {
return (*time.Time)(nil), nil
}
t := untyped.(*time.Time)
return t, nil
}
// ecInProcessSetEndOfWindowUnsynchronized sets the end time of the eventually consistent window.
// There is no mutex synchronization.
func ecInProcessSetEndOfWindowUnsynchronized(e *EventuallyConsistentContext, newEndOfWindowTime *time.Time) error {
e.endOfWindow.Store(newEndOfWindowTime) // atomically replace the current object with the new one
return nil
}
//
// File functions
//
func ecFileReadLock(e *EventuallyConsistentContext) error {
return e.timestampFileLock.RLock()
}
func ecFileReadUnlock(e *EventuallyConsistentContext) error {
return e.timestampFileLock.Unlock()
}
func ecFileWriteLock(e *EventuallyConsistentContext) error {
return e.timestampFileLock.Lock()
}
func ecFileWriteUnlock(e *EventuallyConsistentContext) error {
return e.timestampFileLock.Unlock()
}
// ecFileGetEndOfWindowUnsynchronized returns the end time of an eventually consistent window,
// or nil if no eventually consistent requests were made.
// There is no mutex synchronization.
func ecFileGetEndOfWindowUnsynchronized(e *EventuallyConsistentContext) (*time.Time, error) {
file, err := os.Stat(*e.timestampFileName)
if errors.Is(err, os.ErrNotExist) {
ecDebugf("%s: File '%s' does not exist, meaning no EC in effect", OciGoSdkEcConfigEnvVarName, *e.timestampFileName)
return (*time.Time)(nil), nil
}
if err != nil {
ecLogf("%s: Error getting modified time from file '%s', assuming no EC in effect: %s", OciGoSdkEcConfigEnvVarName, *e.timestampFileName, err)
return (*time.Time)(nil), err
}
t := file.ModTime()
ecDebugf("%s: Read modified time of file '%s' as '%s'", OciGoSdkEcConfigEnvVarName, *e.timestampFileName, t)
return &t, nil
}
// ecFileSetEndOfWindowUnsynchronized sets the end time of the eventually consistent window.
// There is no mutex synchronization.
func ecFileSetEndOfWindowUnsynchronized(e *EventuallyConsistentContext, newEndOfWindowTime *time.Time) error {
if newEndOfWindowTime != nil {
ecDebugf("%s: Updating modified time of file '%s' to '%s'", OciGoSdkEcConfigEnvVarName, *e.timestampFileName, *newEndOfWindowTime)
} else {
ecDebugf("%s: Updating modified time of file '%s' to <nil>", OciGoSdkEcConfigEnvVarName, *e.timestampFileName)
}
if newEndOfWindowTime == nil {
err := os.Remove(*e.timestampFileName)
if err != nil && !errors.Is(err, os.ErrNotExist) {
ecLogf("%s: Error removing file '%s', may draw wrong EC conflusions: %s", OciGoSdkEcConfigEnvVarName, *e.timestampFileName, err)
}
return err
}
atime := time.Now()
var err = os.Chtimes(*e.timestampFileName, atime, *newEndOfWindowTime)
if errors.Is(err, os.ErrNotExist) {
_, createErr := os.Create(*e.timestampFileName)
if createErr != nil {
ecLogf("%s: Error creating file '%s', will have to assume no EC in effect: %s", OciGoSdkEcConfigEnvVarName, *e.timestampFileName, createErr)
return createErr
}
err = os.Chtimes(*e.timestampFileName, atime, *newEndOfWindowTime)
}
if err != nil {
ecLogf("%s: Error changing modified time for file '%s', will have to assume no EC in effect: %s", OciGoSdkEcConfigEnvVarName, *e.timestampFileName, err)
return err
}
return nil
}
//
// General functions for EC window handling, for all EC communication modes
//
// GetEndOfWindow returns the end time an eventually consistent window,
// or nil if no eventually consistent requests were made
func (e *EventuallyConsistentContext) GetEndOfWindow() *time.Time {
e.readLock(e) // synchronize with potential writers
defer e.readUnlock(e)
endOfWindowTime, _ := e.getEndOfWindowUnsynchronized(e)
// TODO: this is noisy logging, consider removing
if endOfWindowTime != nil {
ecDebugln(fmt.Sprintf("EcContext.GetEndOfWindow returns %s", endOfWindowTime))
} else {
ecDebugln("EcContext.GetEndOfWindow returns <nil>")
}
return endOfWindowTime
}
// UpdateEndOfWindow sets the end time of the eventually consistent window the specified
// duration into the future
func (e *EventuallyConsistentContext) UpdateEndOfWindow(windowSize time.Duration) *time.Time {
e.writeLock(e) // synchronize with other potential writers
defer e.writeUnlock(e)
currentEndOfWindowTime, _ := e.getEndOfWindowUnsynchronized(e)
var newEndOfWindowTime = e.timeNowProvider().Add(windowSize)
if currentEndOfWindowTime == nil || newEndOfWindowTime.After(*currentEndOfWindowTime) {
e.setEndOfWindowUnsynchronized(e, &newEndOfWindowTime)
// TODO: this is noisy logging, consider removing
ecDebugln(fmt.Sprintf("EcContext.UpdateEndOfWindow to %s", newEndOfWindowTime))
return &newEndOfWindowTime
}
return currentEndOfWindowTime
}
// setEndTimeOfEventuallyConsistentWindow sets the last time an eventually consistent request was made
// to the specified time
func (e *EventuallyConsistentContext) setEndOfWindow(newTime *time.Time) *time.Time {
e.writeLock(e) // synchronize with other potential writers
defer e.writeUnlock(e)
e.setEndOfWindowUnsynchronized(e, newTime)
// TODO: this is noisy logging, consider removing
if newTime != nil {
ecDebugln(fmt.Sprintf("EcContext.setEndOfWindow to %s", *newTime))
} else {
ecDebugln("EcContext.setEndOfWindow to <nil>")
}
return newTime
}
// EcContext contains the information about the end of the eventually consistent window for this process.
var EcContext = newEcContext()
//
// Logging helpers
//
// getGID returns the Goroutine id. This is purely for logging and debugging.
// See https://blog.sgmansfield.com/2015/12/goroutine-ids/
func getGID() uint64 {
b := make([]byte, 64)
b = b[:runtime.Stack(b, false)]
b = bytes.TrimPrefix(b, []byte("goroutine "))
b = b[:bytes.IndexByte(b, ' ')]
n, _ := strconv.ParseUint(string(b), 10, 64)
return n
}
// some of these errors happen so early, defaultLogger may not have been
// initialized yet.
func initLogIfNecessary() {
if defaultLogger == nil {
l, _ := NewSDKLogger()
SetSDKLogger(l)
}
}
// Debugf logs v with the provided format if debug mode is set.
// There is no mutex synchronization. You should have acquired e.lock first.
func ecDebugf(format string, v ...interface{}) {
defer func() {
// recover from panic if one occured.
if recover() != nil {
Debugln("ecDebugf failed")
}
}()
str := fmt.Sprintf(format, v...)
initLogIfNecessary()
// prefix message with "(pid=25140, gid=5)"
Debugf("(pid=%d, gid=%d) %s", os.Getpid(), getGID(), str)
}
// Debug logs v if debug mode is set.
// There is no mutex synchronization. You should have acquired e.lock first.
func ecDebug(v ...interface{}) {
defer func() {
// recover from panic if one occured.
if recover() != nil {
Debugln("ecDebug failed")
}
}()
initLogIfNecessary()
// prefix message with "(pid=25140, gid=5)"
Debug(append([]interface{}{"(pid=", os.Getpid(), ", gid=", getGID(), ") "}, v...)...)
}
// Debugln logs v appending a new line if debug mode is set
// There is no mutex synchronization. You should have acquired e.lock first.
func ecDebugln(v ...interface{}) {
defer func() {
// recover from panic if one occured.
if recover() != nil {
Debugln("ecDebugln failed")
}
}()
initLogIfNecessary()
// prefix message with "(pid=25140, gid=5)"
Debugln(append([]interface{}{"(pid=", os.Getpid(), ", gid=", getGID(), ") "}, v...)...)
}
// Logf logs v with the provided format if info mode is set.
// There is no mutex synchronization. You should have acquired e.lock first.
func ecLogf(format string, v ...interface{}) {
defer func() {
// recover from panic if one occured.
if recover() != nil {
Debugln("ecLogf failed")
}
}()
initLogIfNecessary()
str := fmt.Sprintf(format, v...)
// prefix message with "(pid=25140, gid=5)"
Logf("(pid=%d, gid=%d) %s", os.Getpid(), getGID(), str)
}

View File

@@ -0,0 +1,308 @@
// Copyright (c) 2016, 2018, 2025, Oracle and/or its affiliates. All rights reserved.
// This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license.
//lint:file-ignore SA1019 older versions of staticcheck (those compatible with Golang 1.17) falsely flag x509.IsEncryptedPEMBlock and x509.DecryptPEMBlock.
package common
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"net/textproto"
"os"
"reflect"
"strconv"
"strings"
"time"
"github.com/youmark/pkcs8"
)
// String returns a pointer to the provided string
func String(value string) *string {
return &value
}
// Int returns a pointer to the provided int
func Int(value int) *int {
return &value
}
// Int64 returns a pointer to the provided int64
func Int64(value int64) *int64 {
return &value
}
// Uint returns a pointer to the provided uint
func Uint(value uint) *uint {
return &value
}
// Float32 returns a pointer to the provided float32
func Float32(value float32) *float32 {
return &value
}
// Float64 returns a pointer to the provided float64
func Float64(value float64) *float64 {
return &value
}
// Bool returns a pointer to the provided bool
func Bool(value bool) *bool {
return &value
}
// PointerString prints the values of pointers in a struct
// Producing a human friendly string for an struct with pointers.
// useful when debugging the values of a struct
func PointerString(datastruct interface{}) (representation string) {
val := reflect.ValueOf(datastruct)
typ := reflect.TypeOf(datastruct)
all := make([]string, 2)
all = append(all, "{")
for i := 0; i < typ.NumField(); i++ {
sf := typ.Field(i)
//unexported
if sf.PkgPath != "" && !sf.Anonymous {
continue
}
sv := val.Field(i)
stringValue := ""
if isNil(sv) {
stringValue = fmt.Sprintf("%s=<nil>", sf.Name)
} else {
if sv.Type().Kind() == reflect.Ptr {
sv = sv.Elem()
}
stringValue = fmt.Sprintf("%s=%v", sf.Name, sv)
}
all = append(all, stringValue)
}
all = append(all, "}")
representation = strings.TrimSpace(strings.Join(all, " "))
return
}
// SDKTime a struct that parses/renders to/from json using RFC339 date-time information
type SDKTime struct {
time.Time
}
// SDKDate a struct that parses/renders to/from json using only date information
type SDKDate struct {
//Date date information
Date time.Time
}
func sdkTimeFromTime(t time.Time) SDKTime {
return SDKTime{t}
}
func sdkDateFromTime(t time.Time) SDKDate {
return SDKDate{Date: t}
}
func formatTime(t SDKTime) string {
return t.Format(sdkTimeFormat)
}
func formatDate(t SDKDate) string {
return t.Date.Format(sdkDateFormat)
}
func now() *SDKTime {
t := SDKTime{time.Now()}
return &t
}
var timeType = reflect.TypeOf(SDKTime{})
var timeTypePtr = reflect.TypeOf(&SDKTime{})
var sdkDateType = reflect.TypeOf(SDKDate{})
var sdkDateTypePtr = reflect.TypeOf(&SDKDate{})
// Formats for sdk supported time representations
const sdkTimeFormat = time.RFC3339Nano
const rfc1123OptionalLeadingDigitsInDay = "Mon, _2 Jan 2006 15:04:05 MST"
const sdkDateFormat = "2006-01-02"
func tryParsingTimeWithValidFormatsForHeaders(data []byte, headerName string) (t time.Time, err error) {
header := strings.ToLower(headerName)
switch header {
case "lastmodified", "date":
t, err = tryParsing(data, time.RFC3339Nano, time.RFC3339, time.RFC1123, rfc1123OptionalLeadingDigitsInDay, time.RFC850, time.ANSIC)
return
default: //By default we parse with RFC3339
t, err = time.Parse(sdkTimeFormat, string(data))
return
}
}
func tryParsing(data []byte, layouts ...string) (tm time.Time, err error) {
datestring := string(data)
for _, l := range layouts {
tm, err = time.Parse(l, datestring)
if err == nil {
return
}
}
err = fmt.Errorf("could not parse time: %s with formats: %s", datestring, layouts[:])
return
}
// String returns string representation of SDKDate
func (t *SDKDate) String() string {
return t.Date.Format(sdkDateFormat)
}
// NewSDKDateFromString parses the dateString into SDKDate
func NewSDKDateFromString(dateString string) (*SDKDate, error) {
parsedTime, err := time.Parse(sdkDateFormat, dateString)
if err != nil {
return nil, err
}
return &SDKDate{Date: parsedTime}, nil
}
// UnmarshalJSON unmarshals from json
func (t *SDKTime) UnmarshalJSON(data []byte) (e error) {
s := string(data)
if s == "null" {
t.Time = time.Time{}
} else {
//Try parsing with RFC3339
t.Time, e = time.Parse(`"`+sdkTimeFormat+`"`, string(data))
}
return
}
// MarshalJSON marshals to JSON
func (t *SDKTime) MarshalJSON() (buff []byte, e error) {
s := t.Format(sdkTimeFormat)
buff = []byte(`"` + s + `"`)
return
}
// UnmarshalJSON unmarshals from json
func (t *SDKDate) UnmarshalJSON(data []byte) (e error) {
if string(data) == `"null"` {
t.Date = time.Time{}
return
}
t.Date, e = tryParsing(data,
strconv.Quote(sdkDateFormat),
)
return
}
// MarshalJSON marshals to JSON
func (t *SDKDate) MarshalJSON() (buff []byte, e error) {
s := t.Date.Format(sdkDateFormat)
buff = []byte(strconv.Quote(s))
return
}
// PrivateKeyFromBytes is a helper function that will produce a RSA private
// key from bytes. This function is deprecated in favour of PrivateKeyFromBytesWithPassword
// Deprecated
func PrivateKeyFromBytes(pemData []byte, password *string) (key *rsa.PrivateKey, e error) {
if password == nil {
return PrivateKeyFromBytesWithPassword(pemData, nil)
}
return PrivateKeyFromBytesWithPassword(pemData, []byte(*password))
}
// PrivateKeyFromBytesWithPassword is a helper function that will produce a RSA private
// key from bytes and a password.
func PrivateKeyFromBytesWithPassword(pemData, password []byte) (key *rsa.PrivateKey, e error) {
pemBlock, _ := pem.Decode(pemData)
if pemBlock == nil {
e = fmt.Errorf("PEM data was not found in buffer")
return
}
decrypted := pemBlock.Bytes
// Support for encrypted PKCS8 format, this format can not be handled by x509.IsEncryptedPEMBlock func
if key, e = pkcs8.ParsePKCS8PrivateKeyRSA(pemBlock.Bytes, password); key != nil {
return
}
// if pemBlock.Type == "ENCRYPTED PRIVATE KEY" {
// return pkcs8.ParsePKCS8PrivateKeyRSA(pemData, password)
// }
if x509.IsEncryptedPEMBlock(pemBlock) {
if password == nil {
return nil, errors.New("private key password is required for encrypted private keys")
}
if decrypted, e = x509.DecryptPEMBlock(pemBlock, password); e != nil {
return
}
}
key, e = parsePKCSPrivateKey(decrypted)
return
}
// ParsePrivateKey using PKCS1 or PKCS8
func parsePKCSPrivateKey(decryptedKey []byte) (*rsa.PrivateKey, error) {
if key, err := x509.ParsePKCS1PrivateKey(decryptedKey); err == nil {
return key, nil
}
if key, err := x509.ParsePKCS8PrivateKey(decryptedKey); err == nil {
switch key := key.(type) {
case *rsa.PrivateKey:
return key, nil
default:
return nil, fmt.Errorf("unsupportesd private key type in PKCS8 wrapping")
}
}
return nil, fmt.Errorf("failed to parse private key")
}
// parseContentLength trims whitespace from cl and returns -1 if can't purse uint, or the value if it's no less than 0
func parseContentLength(cl string) int64 {
cl = textproto.TrimString(cl)
n, err := strconv.ParseUint(cl, 10, 63)
if err != nil {
return -1
}
return int64(n)
}
func generateRandUUID() (string, error) {
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
return "", err
}
uuid := fmt.Sprintf("%x%x%x%x%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
return uuid, nil
}
func makeACopy(original []string) []string {
tmp := make([]string, len(original))
copy(tmp, original)
return tmp
}
// IsEnvVarFalse is used for checking if an environment variable is explicitly set to false, otherwise would set it true by default
func IsEnvVarFalse(envVarKey string) bool {
val, existed := os.LookupEnv(envVarKey)
return existed && strings.ToLower(val) == "false"
}
// IsEnvVarTrue is used for checking if an environment variable is explicitly set to true, otherwise would set it true by default
func IsEnvVarTrue(envVarKey string) bool {
val, existed := os.LookupEnv(envVarKey)
return existed && strings.ToLower(val) == "true"
}

1144
vendor/github.com/oracle/oci-go-sdk/v65/common/http.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,270 @@
// Copyright (c) 2016, 2018, 2025, Oracle and/or its affiliates. All rights reserved.
// This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license.
package common
import (
"bytes"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
)
// HTTPRequestSigner the interface to sign a request
type HTTPRequestSigner interface {
Sign(r *http.Request) error
}
// KeyProvider interface that wraps information about the key's account owner
type KeyProvider interface {
PrivateRSAKey() (*rsa.PrivateKey, error)
KeyID() (string, error)
}
const signerVersion = "1"
// SignerBodyHashPredicate a function that allows to disable/enable body hashing
// of requests and headers associated with body content
type SignerBodyHashPredicate func(r *http.Request) bool
// ociRequestSigner implements the http-signatures-draft spec
// as described in https://tools.ietf.org/html/draft-cavage-http-signatures-08
type ociRequestSigner struct {
KeyProvider KeyProvider
GenericHeaders []string
BodyHeaders []string
ShouldHashBody SignerBodyHashPredicate
}
var (
defaultGenericHeaders = []string{"date", "(request-target)", "host"}
defaultBodyHeaders = []string{"content-length", "content-type", "x-content-sha256"}
defaultBodyHashPredicate = func(r *http.Request) bool {
return r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodPatch
}
)
// DefaultGenericHeaders list of default generic headers that is used in signing
func DefaultGenericHeaders() []string {
return makeACopy(defaultGenericHeaders)
}
// DefaultBodyHeaders list of default body headers that is used in signing
func DefaultBodyHeaders() []string {
return makeACopy(defaultBodyHeaders)
}
// DefaultRequestSigner creates a signer with default parameters.
func DefaultRequestSigner(provider KeyProvider) HTTPRequestSigner {
return RequestSigner(provider, defaultGenericHeaders, defaultBodyHeaders)
}
// RequestSignerExcludeBody creates a signer without hash the body.
func RequestSignerExcludeBody(provider KeyProvider) HTTPRequestSigner {
bodyHashPredicate := func(r *http.Request) bool {
// week request signer will not hash the body
return false
}
return RequestSignerWithBodyHashingPredicate(provider, defaultGenericHeaders, defaultBodyHeaders, bodyHashPredicate)
}
// NewSignerFromOCIRequestSigner creates a copy of the request signer and attaches the new SignerBodyHashPredicate
// returns an error if the passed signer is not of type ociRequestSigner
func NewSignerFromOCIRequestSigner(oldSigner HTTPRequestSigner, predicate SignerBodyHashPredicate) (HTTPRequestSigner, error) {
if oldS, ok := oldSigner.(ociRequestSigner); ok {
s := ociRequestSigner{
KeyProvider: oldS.KeyProvider,
GenericHeaders: oldS.GenericHeaders,
BodyHeaders: oldS.BodyHeaders,
ShouldHashBody: predicate,
}
return s, nil
}
return nil, fmt.Errorf("can not create a signer, input signer needs to be of type ociRequestSigner")
}
// RequestSigner creates a signer that utilizes the specified headers for signing
// and the default predicate for using the body of the request as part of the signature
func RequestSigner(provider KeyProvider, genericHeaders, bodyHeaders []string) HTTPRequestSigner {
return ociRequestSigner{
KeyProvider: provider,
GenericHeaders: genericHeaders,
BodyHeaders: bodyHeaders,
ShouldHashBody: defaultBodyHashPredicate}
}
// RequestSignerWithBodyHashingPredicate creates a signer that utilizes the specified headers for signing, as well as a predicate for using
// the body of the request and bodyHeaders parameter as part of the signature
func RequestSignerWithBodyHashingPredicate(provider KeyProvider, genericHeaders, bodyHeaders []string, shouldHashBody SignerBodyHashPredicate) HTTPRequestSigner {
return ociRequestSigner{
KeyProvider: provider,
GenericHeaders: genericHeaders,
BodyHeaders: bodyHeaders,
ShouldHashBody: shouldHashBody}
}
func (signer ociRequestSigner) getSigningHeaders(r *http.Request) []string {
var result []string
result = append(result, signer.GenericHeaders...)
if signer.ShouldHashBody(r) {
result = append(result, signer.BodyHeaders...)
}
return result
}
func (signer ociRequestSigner) getSigningString(request *http.Request) string {
signingHeaders := signer.getSigningHeaders(request)
signingParts := make([]string, len(signingHeaders))
for i, part := range signingHeaders {
var value string
part = strings.ToLower(part)
switch part {
case "(request-target)":
value = getRequestTarget(request)
case "host":
value = request.URL.Host
if len(value) == 0 {
value = request.Host
}
default:
value = request.Header.Get(part)
}
signingParts[i] = fmt.Sprintf("%s: %s", part, value)
}
signingString := strings.Join(signingParts, "\n")
return signingString
}
func getRequestTarget(request *http.Request) string {
lowercaseMethod := strings.ToLower(request.Method)
return fmt.Sprintf("%s %s", lowercaseMethod, request.URL.RequestURI())
}
func calculateHashOfBody(request *http.Request) (err error) {
var hash string
hash, err = GetBodyHash(request)
if err != nil {
return
}
request.Header.Set(requestHeaderXContentSHA256, hash)
return
}
// drainBody reads all of b to memory and then returns two equivalent
// ReadClosers yielding the same bytes.
//
// It returns an error if the initial slurp of all bytes fails. It does not attempt
// to make the returned ReadClosers have identical error-matching behavior.
func drainBody(b io.ReadCloser) (r1, r2 io.ReadCloser, err error) {
if b == http.NoBody {
// No copying needed. Preserve the magic sentinel meaning of NoBody.
return http.NoBody, http.NoBody, nil
}
var buf bytes.Buffer
if _, err = buf.ReadFrom(b); err != nil {
return nil, b, err
}
if err = b.Close(); err != nil {
return nil, b, err
}
return ioutil.NopCloser(&buf), ioutil.NopCloser(bytes.NewReader(buf.Bytes())), nil
}
func hashAndEncode(data []byte) string {
hashedContent := sha256.Sum256(data)
hash := base64.StdEncoding.EncodeToString(hashedContent[:])
return hash
}
// GetBodyHash creates a base64 string from the hash of body the request
func GetBodyHash(request *http.Request) (hashString string, err error) {
if request.Body == nil {
request.ContentLength = 0
request.Header.Set(requestHeaderContentLength, fmt.Sprintf("%v", request.ContentLength))
return hashAndEncode([]byte("")), nil
}
var data []byte
var bReader io.Reader
bReader, request.Body, err = drainBody(request.Body)
if err != nil {
return "", fmt.Errorf("can not read body of request while calculating body hash: %s", err.Error())
}
data, err = ioutil.ReadAll(bReader)
if err != nil {
return "", fmt.Errorf("can not read body of request while calculating body hash: %s", err.Error())
}
// Since the request can be coming from a binary body. Make an attempt to set the body length
request.ContentLength = int64(len(data))
request.Header.Set(requestHeaderContentLength, fmt.Sprintf("%v", request.ContentLength))
hashString = hashAndEncode(data)
return
}
func (signer ociRequestSigner) computeSignature(request *http.Request) (signature string, err error) {
signingString := signer.getSigningString(request)
hasher := sha256.New()
hasher.Write([]byte(signingString))
hashed := hasher.Sum(nil)
privateKey, err := signer.KeyProvider.PrivateRSAKey()
if err != nil {
return
}
var unencodedSig []byte
unencodedSig, e := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed)
if e != nil {
err = fmt.Errorf("can not compute signature while signing the request %s: ", e.Error())
return
}
signature = base64.StdEncoding.EncodeToString(unencodedSig)
return
}
// Sign signs the http request, by inspecting the necessary headers. Once signed
// the request will have the proper 'Authorization' header set, otherwise
// and error is returned
func (signer ociRequestSigner) Sign(request *http.Request) (err error) {
if signer.ShouldHashBody(request) {
err = calculateHashOfBody(request)
if err != nil {
return
}
}
var signature string
if signature, err = signer.computeSignature(request); err != nil {
return
}
signingHeaders := strings.Join(signer.getSigningHeaders(request), " ")
var keyID string
if keyID, err = signer.KeyProvider.KeyID(); err != nil {
return
}
authValue := fmt.Sprintf("Signature version=\"%s\",headers=\"%s\",keyId=\"%s\",algorithm=\"rsa-sha256\",signature=\"%s\"",
signerVersion, signingHeaders, keyID, signature)
request.Header.Set(requestHeaderAuthorization, authValue)
return
}

231
vendor/github.com/oracle/oci-go-sdk/v65/common/log.go generated vendored Normal file
View File

@@ -0,0 +1,231 @@
// Copyright (c) 2016, 2018, 2025, Oracle and/or its affiliates. All rights reserved.
// This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license.
package common
import (
"fmt"
"io"
"io/ioutil"
"log"
"os"
"strings"
"sync"
"time"
)
// sdkLogger an interface for logging in the SDK
type sdkLogger interface {
//LogLevel returns the log level of sdkLogger
LogLevel() int
//Log logs v with the provided format if the current log level is loglevel
Log(logLevel int, format string, v ...interface{}) error
}
// noLogging no logging messages
const noLogging = 0
// infoLogging minimal logging messages
const infoLogging = 1
// debugLogging some logging messages
const debugLogging = 2
// verboseLogging all logging messages
const verboseLogging = 3
// DefaultSDKLogger the default implementation of the sdkLogger
type DefaultSDKLogger struct {
currentLoggingLevel int
verboseLogger *log.Logger
debugLogger *log.Logger
infoLogger *log.Logger
nullLogger *log.Logger
}
// defaultLogger is the defaultLogger in the SDK
var defaultLogger sdkLogger
var loggerLock sync.Mutex
var file *os.File
// initializes the SDK defaultLogger as a defaultLogger
func init() {
l, _ := NewSDKLogger()
SetSDKLogger(l)
}
// SetSDKLogger sets the logger used by the sdk
func SetSDKLogger(logger sdkLogger) {
loggerLock.Lock()
defaultLogger = logger
loggerLock.Unlock()
}
// NewSDKLogger creates a defaultSDKLogger
// Debug logging is turned on/off by the presence of the environment variable "OCI_GO_SDK_DEBUG"
// The value of the "OCI_GO_SDK_DEBUG" environment variable controls the logging level.
// "null" outputs no log messages
// "i" or "info" outputs minimal log messages
// "d" or "debug" outputs some logs messages
// "v" or "verbose" outputs all logs messages, including body of requests
func NewSDKLogger() (DefaultSDKLogger, error) {
logger := DefaultSDKLogger{}
logger.currentLoggingLevel = noLogging
logger.verboseLogger = log.New(os.Stderr, "VERBOSE ", log.Ldate|log.Lmicroseconds|log.Lshortfile)
logger.debugLogger = log.New(os.Stderr, "DEBUG ", log.Ldate|log.Lmicroseconds|log.Lshortfile)
logger.infoLogger = log.New(os.Stderr, "INFO ", log.Ldate|log.Lmicroseconds|log.Lshortfile)
logger.nullLogger = log.New(ioutil.Discard, "", log.Ldate|log.Lmicroseconds|log.Lshortfile)
configured, isLogEnabled := os.LookupEnv("OCI_GO_SDK_DEBUG")
// If env variable not present turn logging off
if !isLogEnabled {
logger.currentLoggingLevel = noLogging
} else {
logOutputModeConfig(logger)
switch strings.ToLower(configured) {
case "null":
logger.currentLoggingLevel = noLogging
break
case "i", "info":
logger.currentLoggingLevel = infoLogging
break
case "d", "debug":
logger.currentLoggingLevel = debugLogging
break
//1 here for backwards compatibility
case "v", "verbose", "1":
logger.currentLoggingLevel = verboseLogging
break
default:
logger.currentLoggingLevel = infoLogging
}
logger.infoLogger.Println("logger level set to: ", logger.currentLoggingLevel)
}
return logger, nil
}
func (l DefaultSDKLogger) getLoggerForLevel(logLevel int) *log.Logger {
if logLevel > l.currentLoggingLevel {
return l.nullLogger
}
switch logLevel {
case noLogging:
return l.nullLogger
case infoLogging:
return l.infoLogger
case debugLogging:
return l.debugLogger
case verboseLogging:
return l.verboseLogger
default:
return l.nullLogger
}
}
// Set SDK Log output mode
// Output mode is switched based on environment variable "OCI_GO_SDK_LOG_OUPUT_MODE"
// "file" outputs log to a specific file
// "combine" outputs log to both stderr and specific file
// other unsupported value outputs log to stderr
// output file can be set via environment variable "OCI_GO_SDK_LOG_FILE"
// if this environment variable is not set, a default log file will be created under project root path
func logOutputModeConfig(logger DefaultSDKLogger) {
logMode, isLogOutputModeEnabled := os.LookupEnv("OCI_GO_SDK_LOG_OUTPUT_MODE")
if !isLogOutputModeEnabled {
return
}
fileName, isLogFileNameProvided := os.LookupEnv("OCI_GO_SDK_LOG_FILE")
if !isLogFileNameProvided {
fileName = fmt.Sprintf("logging_%v%s", time.Now().Unix(), ".log")
}
switch strings.ToLower(logMode) {
case "file", "f":
file = openLogOutputFile(logger, fileName)
logger.infoLogger.SetOutput(file)
logger.debugLogger.SetOutput(file)
logger.verboseLogger.SetOutput(file)
break
case "combine", "c":
file = openLogOutputFile(logger, fileName)
wrt := io.MultiWriter(os.Stderr, file)
logger.infoLogger.SetOutput(wrt)
logger.debugLogger.SetOutput(wrt)
logger.verboseLogger.SetOutput(wrt)
break
}
}
func openLogOutputFile(logger DefaultSDKLogger, fileName string) *os.File {
file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
logger.verboseLogger.Fatal(err)
}
return file
}
// CloseLogFile close the logging file and return error
func CloseLogFile() error {
return file.Close()
}
// LogLevel returns the current debug level
func (l DefaultSDKLogger) LogLevel() int {
return l.currentLoggingLevel
}
// Log logs v with the provided format if the current log level is loglevel
func (l DefaultSDKLogger) Log(logLevel int, format string, v ...interface{}) error {
logger := l.getLoggerForLevel(logLevel)
logger.Output(4, fmt.Sprintf(format, v...))
return nil
}
// Logln logs v appending a new line at the end
// Deprecated
func Logln(v ...interface{}) {
defaultLogger.Log(infoLogging, "%v\n", v...)
}
// Logf logs v with the provided format
func Logf(format string, v ...interface{}) {
defaultLogger.Log(infoLogging, format, v...)
}
// Debugf logs v with the provided format if debug mode is set
func Debugf(format string, v ...interface{}) {
defaultLogger.Log(debugLogging, format, v...)
}
// Debug logs v if debug mode is set
func Debug(v ...interface{}) {
m := fmt.Sprint(v...)
defaultLogger.Log(debugLogging, "%s", m)
}
// Debugln logs v appending a new line if debug mode is set
func Debugln(v ...interface{}) {
m := fmt.Sprint(v...)
defaultLogger.Log(debugLogging, "%s\n", m)
}
// IfDebug executes closure if debug is enabled
func IfDebug(fn func()) {
if defaultLogger.LogLevel() >= debugLogging {
fn()
}
}
// IfInfo executes closure if info is enabled
func IfInfo(fn func()) {
if defaultLogger.LogLevel() >= infoLogging {
fn()
}
}

View File

@@ -0,0 +1,120 @@
// Copyright (c) 2016, 2018, 2025, Oracle and/or its affiliates. All rights reserved.
// This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license.
package common
import (
"fmt"
"net/http"
"sync"
"time"
)
// OciHTTPTransportWrapper is a http.RoundTripper that periodically refreshes
// the underlying http.Transport according to its templates.
// Upon the first use (or once the RefreshRate duration is elapsed),
// a new transport will be created from the TransportTemplate (if set).
type OciHTTPTransportWrapper struct {
// RefreshRate specifies the duration at which http.Transport
// (with its tls.Config) must be refreshed.
// Defaults to 5 minutes.
RefreshRate time.Duration
// TLSConfigProvider creates a new tls.Config.
// If not set, nil tls.Config is returned.
TLSConfigProvider TLSConfigProvider
// ClientTemplate is responsible for creating a new http.Client with
// a given tls.Config.
//
// If not set, a new http.Client with a cloned http.DefaultTransport is returned.
TransportTemplate TransportTemplateProvider
// mutable properties
mux sync.RWMutex
lastRefreshedAt time.Time
delegate http.RoundTripper
}
// RoundTrip implements http.RoundTripper.
func (t *OciHTTPTransportWrapper) RoundTrip(req *http.Request) (*http.Response, error) {
delegate, err := t.refreshDelegate(false /* force */)
if err != nil {
return nil, err
}
return delegate.RoundTrip(req)
}
// Refresh forces refresh of the underlying delegate.
func (t *OciHTTPTransportWrapper) Refresh(force bool) error {
_, err := t.refreshDelegate(force)
return err
}
// Delegate returns the currently active http.RoundTripper.
// Might be nil.
func (t *OciHTTPTransportWrapper) Delegate() http.RoundTripper {
t.mux.RLock()
defer t.mux.RUnlock()
return t.delegate
}
// refreshDelegate refreshes the delegate (and its TLS config) if:
// - force is true
// - it's been more than RefreshRate since the last time the client was refreshed.
func (t *OciHTTPTransportWrapper) refreshDelegate(force bool) (http.RoundTripper, error) {
// read-lock first, since it's cheaper than write lock
t.mux.RLock()
if !t.shouldRefreshLocked(force) {
delegate := t.delegate
t.mux.RUnlock()
return delegate, nil
}
// upgrade to write-lock, and we'll need to check again for the same condition as above
// to avoid multiple initializations by multiple "refresher" goroutines
t.mux.RUnlock()
t.mux.Lock()
defer t.mux.Unlock()
if !t.shouldRefreshLocked(force) {
return t.delegate, nil
}
// For this check we need the delegate to be set once before we check for change in cert files
if t.delegate != nil && !t.TLSConfigProvider.WatchedFilesModified() {
Debug("No modification in custom certs or ca bundle skipping refresh")
// Updating the last refresh time to make sure the next check is only done after the refresh interval has passed
t.lastRefreshedAt = time.Now()
return t.delegate, nil
}
Logf("Loading tls config from TLSConfigProvider")
tlsConfig, err := t.TLSConfigProvider.NewOrDefault()
if err != nil {
return nil, fmt.Errorf("refreshing tls.Config from template: %w", err)
}
t.delegate, err = t.TransportTemplate.NewOrDefault(tlsConfig)
if err != nil {
return nil, fmt.Errorf("refreshing http.RoundTripper from template: %w", err)
}
t.lastRefreshedAt = time.Now()
return t.delegate, nil
}
// shouldRefreshLocked returns whether the client (and its TLS config)
// needs to be refreshed.
func (t *OciHTTPTransportWrapper) shouldRefreshLocked(force bool) bool {
if force || t.delegate == nil {
return true
}
return t.refreshRate() > 0 && time.Since(t.lastRefreshedAt) > t.refreshRate()
}
func (t *OciHTTPTransportWrapper) refreshRate() time.Duration {
return t.RefreshRate
}

View File

@@ -0,0 +1,351 @@
// Copyright (c) 2016, 2018, 2025, Oracle and/or its affiliates. All rights reserved.
// This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license.
package common
const (
//RegionAPChuncheon1 region Chuncheon
RegionAPChuncheon1 Region = "ap-chuncheon-1"
//RegionAPHyderabad1 region Hyderabad
RegionAPHyderabad1 Region = "ap-hyderabad-1"
//RegionAPMelbourne1 region Melbourne
RegionAPMelbourne1 Region = "ap-melbourne-1"
//RegionAPMumbai1 region Mumbai
RegionAPMumbai1 Region = "ap-mumbai-1"
//RegionAPOsaka1 region Osaka
RegionAPOsaka1 Region = "ap-osaka-1"
//RegionAPSeoul1 region Seoul
RegionAPSeoul1 Region = "ap-seoul-1"
//RegionAPSydney1 region Sydney
RegionAPSydney1 Region = "ap-sydney-1"
//RegionAPTokyo1 region Tokyo
RegionAPTokyo1 Region = "ap-tokyo-1"
//RegionCAMontreal1 region Montreal
RegionCAMontreal1 Region = "ca-montreal-1"
//RegionCAToronto1 region Toronto
RegionCAToronto1 Region = "ca-toronto-1"
//RegionEUAmsterdam1 region Amsterdam
RegionEUAmsterdam1 Region = "eu-amsterdam-1"
//RegionFRA region Frankfurt
RegionFRA Region = "eu-frankfurt-1"
//RegionEUZurich1 region Zurich
RegionEUZurich1 Region = "eu-zurich-1"
//RegionMEJeddah1 region Jeddah
RegionMEJeddah1 Region = "me-jeddah-1"
//RegionMEDubai1 region Dubai
RegionMEDubai1 Region = "me-dubai-1"
//RegionSASaopaulo1 region Saopaulo
RegionSASaopaulo1 Region = "sa-saopaulo-1"
//RegionUKCardiff1 region Cardiff
RegionUKCardiff1 Region = "uk-cardiff-1"
//RegionLHR region London
RegionLHR Region = "uk-london-1"
//RegionIAD region Ashburn
RegionIAD Region = "us-ashburn-1"
//RegionPHX region Phoenix
RegionPHX Region = "us-phoenix-1"
//RegionSJC1 region Sanjose
RegionSJC1 Region = "us-sanjose-1"
//RegionSAVinhedo1 region Vinhedo
RegionSAVinhedo1 Region = "sa-vinhedo-1"
//RegionSASantiago1 region Santiago
RegionSASantiago1 Region = "sa-santiago-1"
//RegionILJerusalem1 region Jerusalem
RegionILJerusalem1 Region = "il-jerusalem-1"
//RegionEUMarseille1 region Marseille
RegionEUMarseille1 Region = "eu-marseille-1"
//RegionAPSingapore1 region Singapore
RegionAPSingapore1 Region = "ap-singapore-1"
//RegionMEAbudhabi1 region Abudhabi
RegionMEAbudhabi1 Region = "me-abudhabi-1"
//RegionEUMilan1 region Milan
RegionEUMilan1 Region = "eu-milan-1"
//RegionEUStockholm1 region Stockholm
RegionEUStockholm1 Region = "eu-stockholm-1"
//RegionAFJohannesburg1 region Johannesburg
RegionAFJohannesburg1 Region = "af-johannesburg-1"
//RegionEUParis1 region Paris
RegionEUParis1 Region = "eu-paris-1"
//RegionMXQueretaro1 region Queretaro
RegionMXQueretaro1 Region = "mx-queretaro-1"
//RegionEUMadrid1 region Madrid
RegionEUMadrid1 Region = "eu-madrid-1"
//RegionUSChicago1 region Chicago
RegionUSChicago1 Region = "us-chicago-1"
//RegionMXMonterrey1 region Monterrey
RegionMXMonterrey1 Region = "mx-monterrey-1"
//RegionUSSaltlake2 region Saltlake
RegionUSSaltlake2 Region = "us-saltlake-2"
//RegionSABogota1 region Bogota
RegionSABogota1 Region = "sa-bogota-1"
//RegionSAValparaiso1 region Valparaiso
RegionSAValparaiso1 Region = "sa-valparaiso-1"
//RegionAPSingapore2 region Singapore
RegionAPSingapore2 Region = "ap-singapore-2"
//RegionMERiyadh1 region Riyadh
RegionMERiyadh1 Region = "me-riyadh-1"
//RegionAPDelhi1 region Delhi
RegionAPDelhi1 Region = "ap-delhi-1"
//RegionUSLangley1 region Langley
RegionUSLangley1 Region = "us-langley-1"
//RegionUSLuke1 region Luke
RegionUSLuke1 Region = "us-luke-1"
//RegionUSGovAshburn1 gov region Ashburn
RegionUSGovAshburn1 Region = "us-gov-ashburn-1"
//RegionUSGovChicago1 gov region Chicago
RegionUSGovChicago1 Region = "us-gov-chicago-1"
//RegionUSGovPhoenix1 gov region Phoenix
RegionUSGovPhoenix1 Region = "us-gov-phoenix-1"
//RegionUKGovLondon1 gov region London
RegionUKGovLondon1 Region = "uk-gov-london-1"
//RegionUKGovCardiff1 gov region Cardiff
RegionUKGovCardiff1 Region = "uk-gov-cardiff-1"
//RegionAPChiyoda1 region Chiyoda
RegionAPChiyoda1 Region = "ap-chiyoda-1"
//RegionAPIbaraki1 region Ibaraki
RegionAPIbaraki1 Region = "ap-ibaraki-1"
//RegionMEDccMuscat1 region Muscat
RegionMEDccMuscat1 Region = "me-dcc-muscat-1"
//RegionAPDccCanberra1 region Canberra
RegionAPDccCanberra1 Region = "ap-dcc-canberra-1"
//RegionEUDccMilan1 region Milan
RegionEUDccMilan1 Region = "eu-dcc-milan-1"
//RegionEUDccMilan2 region Milan
RegionEUDccMilan2 Region = "eu-dcc-milan-2"
//RegionEUDccDublin2 region Dublin
RegionEUDccDublin2 Region = "eu-dcc-dublin-2"
//RegionEUDccRating2 region Rating
RegionEUDccRating2 Region = "eu-dcc-rating-2"
//RegionEUDccRating1 region Rating
RegionEUDccRating1 Region = "eu-dcc-rating-1"
//RegionEUDccDublin1 region Dublin
RegionEUDccDublin1 Region = "eu-dcc-dublin-1"
//RegionAPDccGazipur1 region Gazipur
RegionAPDccGazipur1 Region = "ap-dcc-gazipur-1"
//RegionEUMadrid2 region Madrid
RegionEUMadrid2 Region = "eu-madrid-2"
//RegionEUFrankfurt2 region Frankfurt
RegionEUFrankfurt2 Region = "eu-frankfurt-2"
//RegionEUJovanovac1 region Jovanovac
RegionEUJovanovac1 Region = "eu-jovanovac-1"
//RegionMEDccDoha1 region Doha
RegionMEDccDoha1 Region = "me-dcc-doha-1"
//RegionUSSomerset1 region Somerset
RegionUSSomerset1 Region = "us-somerset-1"
//RegionUSThames1 region Thames
RegionUSThames1 Region = "us-thames-1"
//RegionEUDccZurich1 region Zurich
RegionEUDccZurich1 Region = "eu-dcc-zurich-1"
//RegionEUCrissier1 region Crissier
RegionEUCrissier1 Region = "eu-crissier-1"
//RegionMEAbudhabi3 region Abudhabi
RegionMEAbudhabi3 Region = "me-abudhabi-3"
//RegionMEAlain1 region Alain
RegionMEAlain1 Region = "me-alain-1"
//RegionMEAbudhabi2 region Abudhabi
RegionMEAbudhabi2 Region = "me-abudhabi-2"
//RegionMEAbudhabi4 region Abudhabi
RegionMEAbudhabi4 Region = "me-abudhabi-4"
//RegionAPSeoul2 region Seoul
RegionAPSeoul2 Region = "ap-seoul-2"
//RegionAPSuwon1 region Suwon
RegionAPSuwon1 Region = "ap-suwon-1"
//RegionAPChuncheon2 region Chuncheon
RegionAPChuncheon2 Region = "ap-chuncheon-2"
//RegionUSAshburn2 region Ashburn
RegionUSAshburn2 Region = "us-ashburn-2"
)
var shortNameRegion = map[string]Region{
"yny": RegionAPChuncheon1,
"hyd": RegionAPHyderabad1,
"mel": RegionAPMelbourne1,
"bom": RegionAPMumbai1,
"kix": RegionAPOsaka1,
"icn": RegionAPSeoul1,
"syd": RegionAPSydney1,
"nrt": RegionAPTokyo1,
"yul": RegionCAMontreal1,
"yyz": RegionCAToronto1,
"ams": RegionEUAmsterdam1,
"fra": RegionFRA,
"zrh": RegionEUZurich1,
"jed": RegionMEJeddah1,
"dxb": RegionMEDubai1,
"gru": RegionSASaopaulo1,
"cwl": RegionUKCardiff1,
"lhr": RegionLHR,
"iad": RegionIAD,
"phx": RegionPHX,
"sjc": RegionSJC1,
"vcp": RegionSAVinhedo1,
"scl": RegionSASantiago1,
"mtz": RegionILJerusalem1,
"mrs": RegionEUMarseille1,
"sin": RegionAPSingapore1,
"auh": RegionMEAbudhabi1,
"lin": RegionEUMilan1,
"arn": RegionEUStockholm1,
"jnb": RegionAFJohannesburg1,
"cdg": RegionEUParis1,
"qro": RegionMXQueretaro1,
"mad": RegionEUMadrid1,
"ord": RegionUSChicago1,
"mty": RegionMXMonterrey1,
"aga": RegionUSSaltlake2,
"bog": RegionSABogota1,
"vap": RegionSAValparaiso1,
"xsp": RegionAPSingapore2,
"ruh": RegionMERiyadh1,
"onm": RegionAPDelhi1,
"lfi": RegionUSLangley1,
"luf": RegionUSLuke1,
"ric": RegionUSGovAshburn1,
"pia": RegionUSGovChicago1,
"tus": RegionUSGovPhoenix1,
"ltn": RegionUKGovLondon1,
"brs": RegionUKGovCardiff1,
"nja": RegionAPChiyoda1,
"ukb": RegionAPIbaraki1,
"mct": RegionMEDccMuscat1,
"wga": RegionAPDccCanberra1,
"bgy": RegionEUDccMilan1,
"mxp": RegionEUDccMilan2,
"snn": RegionEUDccDublin2,
"dtm": RegionEUDccRating2,
"dus": RegionEUDccRating1,
"ork": RegionEUDccDublin1,
"dac": RegionAPDccGazipur1,
"vll": RegionEUMadrid2,
"str": RegionEUFrankfurt2,
"beg": RegionEUJovanovac1,
"doh": RegionMEDccDoha1,
"ebb": RegionUSSomerset1,
"ebl": RegionUSThames1,
"avz": RegionEUDccZurich1,
"avf": RegionEUCrissier1,
"ahu": RegionMEAbudhabi3,
"rba": RegionMEAlain1,
"rkt": RegionMEAbudhabi2,
"shj": RegionMEAbudhabi4,
"dtz": RegionAPSeoul2,
"dln": RegionAPSuwon1,
"bno": RegionAPChuncheon2,
"yxj": RegionUSAshburn2,
}
var realm = map[string]string{
"oc1": "oraclecloud.com",
"oc2": "oraclegovcloud.com",
"oc3": "oraclegovcloud.com",
"oc4": "oraclegovcloud.uk",
"oc8": "oraclecloud8.com",
"oc9": "oraclecloud9.com",
"oc10": "oraclecloud10.com",
"oc14": "oraclecloud14.com",
"oc15": "oraclecloud15.com",
"oc19": "oraclecloud.eu",
"oc20": "oraclecloud20.com",
"oc21": "oraclecloud21.com",
"oc23": "oraclecloud23.com",
"oc24": "oraclecloud24.com",
"oc26": "oraclecloud26.com",
"oc29": "oraclecloud29.com",
"oc35": "oraclecloud35.com",
"oc42": "oraclecloud42.com",
}
var regionRealm = map[Region]string{
RegionAPChuncheon1: "oc1",
RegionAPHyderabad1: "oc1",
RegionAPMelbourne1: "oc1",
RegionAPMumbai1: "oc1",
RegionAPOsaka1: "oc1",
RegionAPSeoul1: "oc1",
RegionAPSydney1: "oc1",
RegionAPTokyo1: "oc1",
RegionCAMontreal1: "oc1",
RegionCAToronto1: "oc1",
RegionEUAmsterdam1: "oc1",
RegionFRA: "oc1",
RegionEUZurich1: "oc1",
RegionMEJeddah1: "oc1",
RegionMEDubai1: "oc1",
RegionSASaopaulo1: "oc1",
RegionUKCardiff1: "oc1",
RegionLHR: "oc1",
RegionIAD: "oc1",
RegionPHX: "oc1",
RegionSJC1: "oc1",
RegionSAVinhedo1: "oc1",
RegionSASantiago1: "oc1",
RegionILJerusalem1: "oc1",
RegionEUMarseille1: "oc1",
RegionAPSingapore1: "oc1",
RegionMEAbudhabi1: "oc1",
RegionEUMilan1: "oc1",
RegionEUStockholm1: "oc1",
RegionAFJohannesburg1: "oc1",
RegionEUParis1: "oc1",
RegionMXQueretaro1: "oc1",
RegionEUMadrid1: "oc1",
RegionUSChicago1: "oc1",
RegionMXMonterrey1: "oc1",
RegionUSSaltlake2: "oc1",
RegionSABogota1: "oc1",
RegionSAValparaiso1: "oc1",
RegionAPSingapore2: "oc1",
RegionMERiyadh1: "oc1",
RegionAPDelhi1: "oc1",
RegionUSLangley1: "oc2",
RegionUSLuke1: "oc2",
RegionUSGovAshburn1: "oc3",
RegionUSGovChicago1: "oc3",
RegionUSGovPhoenix1: "oc3",
RegionUKGovLondon1: "oc4",
RegionUKGovCardiff1: "oc4",
RegionAPChiyoda1: "oc8",
RegionAPIbaraki1: "oc8",
RegionMEDccMuscat1: "oc9",
RegionAPDccCanberra1: "oc10",
RegionEUDccMilan1: "oc14",
RegionEUDccMilan2: "oc14",
RegionEUDccDublin2: "oc14",
RegionEUDccRating2: "oc14",
RegionEUDccRating1: "oc14",
RegionEUDccDublin1: "oc14",
RegionAPDccGazipur1: "oc15",
RegionEUMadrid2: "oc19",
RegionEUFrankfurt2: "oc19",
RegionEUJovanovac1: "oc20",
RegionMEDccDoha1: "oc21",
RegionUSSomerset1: "oc23",
RegionUSThames1: "oc23",
RegionEUDccZurich1: "oc24",
RegionEUCrissier1: "oc24",
RegionMEAbudhabi3: "oc26",
RegionMEAlain1: "oc26",
RegionMEAbudhabi2: "oc29",
RegionMEAbudhabi4: "oc29",
RegionAPSeoul2: "oc35",
RegionAPSuwon1: "oc35",
RegionAPChuncheon2: "oc35",
RegionUSAshburn2: "oc42",
}

View File

@@ -0,0 +1,452 @@
[
{
"regionKey": "yny",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "ap-chuncheon-1",
"realmKey": "oc1"
},
{
"regionKey": "hyd",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "ap-hyderabad-1",
"realmKey": "oc1"
},
{
"regionKey": "mel",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "ap-melbourne-1",
"realmKey": "oc1"
},
{
"regionKey": "bom",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "ap-mumbai-1",
"realmKey": "oc1"
},
{
"regionKey": "kix",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "ap-osaka-1",
"realmKey": "oc1"
},
{
"regionKey": "icn",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "ap-seoul-1",
"realmKey": "oc1"
},
{
"regionKey": "syd",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "ap-sydney-1",
"realmKey": "oc1"
},
{
"regionKey": "nrt",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "ap-tokyo-1",
"realmKey": "oc1"
},
{
"regionKey": "yul",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "ca-montreal-1",
"realmKey": "oc1"
},
{
"regionKey": "yyz",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "ca-toronto-1",
"realmKey": "oc1"
},
{
"regionKey": "ams",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "eu-amsterdam-1",
"realmKey": "oc1"
},
{
"regionKey": "fra",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "eu-frankfurt-1",
"realmKey": "oc1"
},
{
"regionKey": "zrh",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "eu-zurich-1",
"realmKey": "oc1"
},
{
"regionKey": "jed",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "me-jeddah-1",
"realmKey": "oc1"
},
{
"regionKey": "dxb",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "me-dubai-1",
"realmKey": "oc1"
},
{
"regionKey": "gru",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "sa-saopaulo-1",
"realmKey": "oc1"
},
{
"regionKey": "cwl",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "uk-cardiff-1",
"realmKey": "oc1"
},
{
"regionKey": "lhr",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "uk-london-1",
"realmKey": "oc1"
},
{
"regionKey": "iad",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "us-ashburn-1",
"realmKey": "oc1"
},
{
"regionKey": "phx",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "us-phoenix-1",
"realmKey": "oc1"
},
{
"regionKey": "sjc",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "us-sanjose-1",
"realmKey": "oc1"
},
{
"regionKey": "vcp",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "sa-vinhedo-1",
"realmKey": "oc1"
},
{
"regionKey": "scl",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "sa-santiago-1",
"realmKey": "oc1"
},
{
"regionKey": "lfi",
"realmDomainComponent": "oraclegovcloud.com",
"regionIdentifier": "us-langley-1",
"realmKey": "oc2"
},
{
"regionKey": "luf",
"realmDomainComponent": "oraclegovcloud.com",
"regionIdentifier": "us-luke-1",
"realmKey": "oc2"
},
{
"regionKey": "ric",
"realmDomainComponent": "oraclegovcloud.com",
"regionIdentifier": "us-gov-ashburn-1",
"realmKey": "oc3"
},
{
"regionKey": "pia",
"realmDomainComponent": "oraclegovcloud.com",
"regionIdentifier": "us-gov-chicago-1",
"realmKey": "oc3"
},
{
"regionKey": "tus",
"realmDomainComponent": "oraclegovcloud.com",
"regionIdentifier": "us-gov-phoenix-1",
"realmKey": "oc3"
},
{
"regionKey": "ltn",
"realmDomainComponent": "oraclegovcloud.uk",
"regionIdentifier": "uk-gov-london-1",
"realmKey": "oc4"
},
{
"regionKey": "brs",
"realmDomainComponent": "oraclegovcloud.uk",
"regionIdentifier": "uk-gov-cardiff-1",
"realmKey": "oc4"
},
{
"regionKey": "nja",
"realmDomainComponent": "oraclecloud8.com",
"regionIdentifier": "ap-chiyoda-1",
"realmKey": "oc8"
},
{
"regionKey": "ukb",
"realmDomainComponent": "oraclecloud8.com",
"regionIdentifier": "ap-ibaraki-1",
"realmKey": "oc8"
},
{
"regionKey": "mtz",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "il-jerusalem-1",
"realmKey": "oc1"
},
{
"regionKey": "mrs",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "eu-marseille-1",
"realmKey": "oc1"
},
{
"regionKey": "sin",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "ap-singapore-1",
"realmKey": "oc1"
},
{
"regionKey": "auh",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "me-abudhabi-1",
"realmKey": "oc1"
},
{
"regionKey": "lin",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "eu-milan-1",
"realmKey": "oc1"
},
{
"regionKey": "arn",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "eu-stockholm-1",
"realmKey": "oc1"
},
{
"regionKey": "jnb",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "af-johannesburg-1",
"realmKey": "oc1"
},
{
"regionKey": "mct",
"realmDomainComponent": "oraclecloud9.com",
"regionIdentifier": "me-dcc-muscat-1",
"realmKey": "oc9"
},
{
"regionKey": "wga",
"realmDomainComponent": "oraclecloud10.com",
"regionIdentifier": "ap-dcc-canberra-1",
"realmKey": "oc10"
},
{
"regionKey": "cdg",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "eu-paris-1",
"realmKey": "oc1"
},
{
"regionKey": "qro",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "mx-queretaro-1",
"realmKey": "oc1"
},
{
"regionKey": "mad",
"realmDomainComponent": "oraclecloud.com",
"regionIdentifier": "eu-madrid-1",
"realmKey": "oc1"
},
{
"regionKey": "bgy",
"realmDomainComponent": "oraclecloud14.com",
"regionIdentifier": "eu-dcc-milan-1",
"realmKey": "oc14"
},
{
"regionKey": "ord",
"realmKey": "oc1",
"regionIdentifier": "us-chicago-1",
"realmDomainComponent": "oraclecloud.com"
},
{
"regionKey": "mxp",
"realmKey": "oc14",
"regionIdentifier": "eu-dcc-milan-2",
"realmDomainComponent": "oraclecloud14.com"
},
{
"regionKey": "snn",
"realmKey": "oc14",
"regionIdentifier": "eu-dcc-dublin-2",
"realmDomainComponent": "oraclecloud14.com"
},
{
"regionKey": "dtm",
"realmKey": "oc14",
"regionIdentifier": "eu-dcc-rating-2",
"realmDomainComponent": "oraclecloud14.com"
},
{
"regionKey": "dus",
"realmKey": "oc14",
"regionIdentifier": "eu-dcc-rating-1",
"realmDomainComponent": "oraclecloud14.com"
},
{
"regionKey": "ork",
"realmKey": "oc14",
"regionIdentifier": "eu-dcc-dublin-1",
"realmDomainComponent": "oraclecloud14.com"
},
{
"regionKey": "beg",
"realmKey": "oc20",
"regionIdentifier": "eu-jovanovac-1",
"realmDomainComponent": "oraclecloud20.com"
},
{
"regionKey": "vll",
"realmKey": "oc19",
"regionIdentifier": "eu-madrid-2",
"realmDomainComponent": "oraclecloud.eu"
},
{
"regionKey": "str",
"realmKey": "oc19",
"regionIdentifier": "eu-frankfurt-2",
"realmDomainComponent": "oraclecloud.eu"
},
{
"regionKey": "mty",
"realmKey": "oc1",
"regionIdentifier": "mx-monterrey-1",
"realmDomainComponent": "oraclecloud.com"
},
{
"regionKey": "aga",
"realmKey": "oc1",
"regionIdentifier": "us-saltlake-2",
"realmDomainComponent": "oraclecloud.com"
},
{
"regionKey": "avz",
"realmKey": "oc24",
"regionIdentifier": "eu-dcc-zurich-1",
"realmDomainComponent": "oraclecloud24.com"
},
{
"regionKey": "bog",
"realmKey": "oc1",
"regionIdentifier": "sa-bogota-1",
"realmDomainComponent": "oraclecloud.com"
},
{
"regionKey": "vap",
"realmKey": "oc1",
"regionIdentifier": "sa-valparaiso-1",
"realmDomainComponent": "oraclecloud.com"
},
{
"regionKey": "doh",
"realmKey": "oc21",
"regionIdentifier": "me-dcc-doha-1",
"realmDomainComponent": "oraclecloud21.com"
},
{
"regionKey": "ahu",
"realmKey": "oc26",
"regionIdentifier": "me-abudhabi-3",
"realmDomainComponent": "oraclecloud26.com"
},
{
"regionKey": "dac",
"realmKey": "oc15",
"regionIdentifier": "ap-dcc-gazipur-1",
"realmDomainComponent": "oraclecloud15.com"
},
{
"regionKey": "xsp",
"realmKey": "oc1",
"regionIdentifier": "ap-singapore-2",
"realmDomainComponent": "oraclecloud.com"
},
{
"regionKey": "rkt",
"realmKey": "oc29",
"regionIdentifier": "me-abudhabi-2",
"realmDomainComponent": "oraclecloud29.com"
},
{
"regionKey": "ruh",
"realmKey": "oc1",
"regionIdentifier": "me-riyadh-1",
"realmDomainComponent": "oraclecloud.com"
},
{
"regionKey": "shj",
"realmKey": "oc29",
"regionIdentifier": "me-abudhabi-4",
"realmDomainComponent": "oraclecloud29.com"
},
{
"regionKey": "avf",
"realmKey": "oc24",
"regionIdentifier": "eu-crissier-1",
"realmDomainComponent": "oraclecloud24.com"
},
{
"regionKey": "ebb",
"realmKey": "oc23",
"regionIdentifier": "us-somerset-1",
"realmDomainComponent": "oraclecloud23.com"
},
{
"regionKey": "ebl",
"realmKey": "oc23",
"regionIdentifier": "us-thames-1",
"realmDomainComponent": "oraclecloud23.com"
},
{
"regionKey": "dtz",
"realmKey": "oc35",
"regionIdentifier": "ap-seoul-2",
"realmDomainComponent": "oraclecloud35.com"
},
{
"regionKey": "dln",
"realmKey": "oc35",
"regionIdentifier": "ap-suwon-1",
"realmDomainComponent": "oraclecloud35.com"
},
{
"regionKey": "bno",
"realmKey": "oc35",
"regionIdentifier": "ap-chuncheon-2",
"realmDomainComponent": "oraclecloud35.com"
},
{
"regionKey": "rba",
"realmKey": "oc26",
"regionIdentifier": "me-alain-1",
"realmDomainComponent": "oraclecloud26.com"
},
{
"regionKey": "yxj",
"realmKey": "oc42",
"regionIdentifier": "us-ashburn-2",
"realmDomainComponent": "oraclecloud42.com"
},
{
"regionKey": "onm",
"realmKey": "oc1",
"regionIdentifier": "ap-delhi-1",
"realmDomainComponent": "oraclecloud.com"
}
]

911
vendor/github.com/oracle/oci-go-sdk/v65/common/retry.go generated vendored Normal file
View File

@@ -0,0 +1,911 @@
// Copyright (c) 2016, 2018, 2025, Oracle and/or its affiliates. All rights reserved.
// This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license.
package common
import (
"context"
"errors"
"fmt"
"io"
"math"
"math/rand"
"runtime"
"strings"
"time"
)
const (
// UnlimitedNumAttemptsValue is the value for indicating unlimited attempts for reaching success
UnlimitedNumAttemptsValue = uint(0)
// number of characters contained in the generated retry token
generatedRetryTokenLength = 32
)
// OCIRetryableRequest represents a request that can be reissued according to the specified policy.
type OCIRetryableRequest interface {
// Any retryable request must implement the OCIRequest interface
OCIRequest
// Each operation should implement this method, if has binary body, return OCIReadSeekCloser and true, otherwise return nil, false
BinaryRequestBody() (*OCIReadSeekCloser, bool)
// Each operation specifies default retry behavior. By passing no arguments to this method, the default retry
// behavior, as determined on a per-operation-basis, will be honored. Variadic retry policy option arguments
// passed to this method will override the default behavior.
RetryPolicy() *RetryPolicy
}
// OCIOperationResponse represents the output of an OCIOperation, with additional context of error message
// and operation attempt number.
type OCIOperationResponse struct {
// Response from OCI Operation
Response OCIResponse
// Error from OCI Operation
Error error
// Operation Attempt Number (one-based)
AttemptNumber uint
// End of eventually consistent effects, or nil if no such effects
EndOfWindowTime *time.Time
// Backoff scaling factor (only used for dealing with eventual consistency)
BackoffScalingFactor float64
// Time of the initial attempt
InitialAttemptTime time.Time
}
const (
defaultMaximumNumberAttempts = uint(8)
defaultExponentialBackoffBase = 2.0
defaultMinSleepBetween = 0.0
defaultMaxSleepBetween = 30.0
ecMaximumNumberAttempts = uint(9)
ecExponentialBackoffBase = 3.52
ecMinSleepBetween = 0.0
ecMaxSleepBetween = 45.0
)
var (
defaultRetryStatusCodeMap = map[StatErrCode]bool{
{409, "IncorrectState"}: true,
{429, "TooManyRequests"}: true,
{501, "MethodNotImplemented"}: false,
}
)
// IsErrorRetryableByDefault returns true if the error is retryable by OCI default retry policy
func IsErrorRetryableByDefault(err error) bool {
if err == nil {
return false
}
if IsNetworkError(err) {
return true
}
if err == io.EOF {
return true
}
if err, ok := IsServiceError(err); ok {
if shouldRetry, ok := defaultRetryStatusCodeMap[StatErrCode{err.GetHTTPStatusCode(), err.GetCode()}]; ok {
return shouldRetry
}
return 500 <= err.GetHTTPStatusCode() && err.GetHTTPStatusCode() < 505
}
return false
}
// NewOCIOperationResponse assembles an OCI Operation Response object.
// Note that InitialAttemptTime is not set, nor is EndOfWindowTime, and BackoffScalingFactor is set to 1.0.
// EndOfWindowTime and BackoffScalingFactor are only important for eventual consistency.
// InitialAttemptTime can be useful for time-based (as opposed to count-based) retry policies.
func NewOCIOperationResponse(response OCIResponse, err error, attempt uint) OCIOperationResponse {
return OCIOperationResponse{
Response: response,
Error: err,
AttemptNumber: attempt,
BackoffScalingFactor: 1.0,
}
}
// NewOCIOperationResponseExtended assembles an OCI Operation Response object, with the value for the EndOfWindowTime, BackoffScalingFactor, and InitialAttemptTime set.
// EndOfWindowTime and BackoffScalingFactor are only important for eventual consistency.
// InitialAttemptTime can be useful for time-based (as opposed to count-based) retry policies.
func NewOCIOperationResponseExtended(response OCIResponse, err error, attempt uint, endOfWindowTime *time.Time, backoffScalingFactor float64,
initialAttemptTime time.Time) OCIOperationResponse {
return OCIOperationResponse{
Response: response,
Error: err,
AttemptNumber: attempt,
EndOfWindowTime: endOfWindowTime,
BackoffScalingFactor: backoffScalingFactor,
InitialAttemptTime: initialAttemptTime,
}
}
//
// RetryPolicy
//
// RetryPolicy is the class that holds all relevant information for retrying operations.
type RetryPolicy struct {
// MaximumNumberAttempts is the maximum number of times to retry a request. Zero indicates an unlimited
// number of attempts.
MaximumNumberAttempts uint
// ShouldRetryOperation inspects the http response, error, and operation attempt number, and
// - returns true if we should retry the operation
// - returns false otherwise
ShouldRetryOperation func(OCIOperationResponse) bool
// GetNextDuration computes the duration to pause between operation retries.
NextDuration func(OCIOperationResponse) time.Duration
// minimum sleep between attempts in seconds
MinSleepBetween float64
// maximum sleep between attempts in seconds
MaxSleepBetween float64
// the base for the exponential backoff
ExponentialBackoffBase float64
// DeterminePolicyToUse may modify the policy to handle eventual consistency; the return values are
// the retry policy to use, the end of the eventually consistent time window, and the backoff scaling factor
// If eventual consistency is not considered, this function should return the unmodified policy that was
// provided as input, along with (*time.Time)(nil) (no time window), and 1.0 (unscaled backoff).
DeterminePolicyToUse func(policy RetryPolicy) (RetryPolicy, *time.Time, float64)
// if the retry policy considers eventual consistency, but there is no eventual consistency present
// the retries will fall back to the policy specified here; recommendation is to set this to DefaultRetryPolicyWithoutEventualConsistency()
NonEventuallyConsistentPolicy *RetryPolicy
// Stores the maximum cumulative backoff in seconds. This can usually be calculated using
// MaximumNumberAttempts, MinSleepBetween, MaxSleepBetween, and ExponentialBackoffBase,
// but if MaximumNumberAttempts is 0 (unlimited attempts), then this needs to be set explicitly
// for Eventual Consistency retries to work.
MaximumCumulativeBackoffWithoutJitter float64
}
// GlobalRetry is user defined global level retry policy, it would impact all services, the precedence is lower
// than user defined client/request level retry policy
var GlobalRetry *RetryPolicy = nil
// RetryPolicyOption is the type of the options for NewRetryPolicy.
type RetryPolicyOption func(rp *RetryPolicy)
// String Converts retry policy to human-readable string representation
func (rp RetryPolicy) String() string {
return fmt.Sprintf("{MaximumNumberAttempts=%v, MinSleepBetween=%v, MaxSleepBetween=%v, ExponentialBackoffBase=%v, NonEventuallyConsistentPolicy=%v}",
rp.MaximumNumberAttempts, rp.MinSleepBetween, rp.MaxSleepBetween, rp.ExponentialBackoffBase, rp.NonEventuallyConsistentPolicy)
}
// Validate returns true if the RetryPolicy is valid; if not, it also returns an error.
func (rp *RetryPolicy) validate() (success bool, err error) {
var errorStrings []string
if rp.ShouldRetryOperation == nil {
errorStrings = append(errorStrings, "ShouldRetryOperation may not be nil")
}
if rp.NextDuration == nil {
errorStrings = append(errorStrings, "NextDuration may not be nil")
}
if rp.NonEventuallyConsistentPolicy != nil {
if rp.MaximumNumberAttempts == 0 && rp.MaximumCumulativeBackoffWithoutJitter <= 0 {
errorStrings = append(errorStrings, "If eventual consistency is handled, and the MaximumNumberAttempts of the EC retry policy is 0 (unlimited attempts), then the MaximumCumulativeBackoffWithoutJitter of the EC retry policy must be positive; used WithUnlimitedAttempts instead")
}
nonEcRp := rp.NonEventuallyConsistentPolicy
if nonEcRp.MaximumNumberAttempts == 0 && nonEcRp.MaximumCumulativeBackoffWithoutJitter <= 0 {
errorStrings = append(errorStrings, "If eventual consistency is handled, and the MaximumNumberAttempts of the non-EC retry policy is 0 (unlimited attempts), then the MaximumCumulativeBackoffWithoutJitter of the non-EC retry policy must be positive; used WithUnlimitedAttempts instead")
}
}
if len(errorStrings) > 0 {
return false, errors.New(strings.Join(errorStrings, ", "))
}
// some legacy code constructing RetryPolicy instances directly may not have set DeterminePolicyToUse.
// In that case, just assume that it doesn't handle eventual consistency.
if rp.DeterminePolicyToUse == nil {
rp.DeterminePolicyToUse = returnSamePolicy
}
return true, nil
}
// GetMaximumCumulativeBackoffWithoutJitter returns the maximum cumulative backoff the retry policy would do,
// taking into account whether eventually consistency is considered or not.
// This function uses either GetMaximumCumulativeBackoffWithoutJitter or GetMaximumCumulativeEventuallyConsistentBackoffWithoutJitter,
// whichever is appropriate
func (rp RetryPolicy) GetMaximumCumulativeBackoffWithoutJitter() time.Duration {
if rp.NonEventuallyConsistentPolicy == nil {
return GetMaximumCumulativeBackoffWithoutJitter(rp)
}
return GetMaximumCumulativeEventuallyConsistentBackoffWithoutJitter(rp)
}
//
// Functions to calculate backoff and maximum cumulative backoff
//
// GetBackoffWithoutJitter calculates the backoff without jitter for the attempt, given the retry policy.
func GetBackoffWithoutJitter(policy RetryPolicy, attempt uint) time.Duration {
return time.Duration(getBackoffWithoutJitterHelper(policy.MinSleepBetween, policy.MaxSleepBetween, policy.ExponentialBackoffBase, attempt)) * time.Second
}
// getBackoffWithoutJitterHelper calculates the backoff without jitter for the attempt, given the loose retry policy values.
func getBackoffWithoutJitterHelper(minSleepBetween float64, maxSleepBetween float64, exponentialBackoffBase float64, attempt uint) float64 {
sleepTime := math.Pow(exponentialBackoffBase, float64(attempt-1))
if sleepTime < minSleepBetween {
sleepTime = minSleepBetween
}
if sleepTime > maxSleepBetween {
sleepTime = maxSleepBetween
}
return sleepTime
}
// GetMaximumCumulativeBackoffWithoutJitter calculates the maximum backoff without jitter, according to the retry
// policy, if every retry attempt is made.
func GetMaximumCumulativeBackoffWithoutJitter(policy RetryPolicy) time.Duration {
return getMaximumCumulativeBackoffWithoutJitterHelper(policy.MinSleepBetween, policy.MaxSleepBetween, policy.ExponentialBackoffBase, policy.MaximumNumberAttempts, policy.MaximumCumulativeBackoffWithoutJitter)
}
func getMaximumCumulativeBackoffWithoutJitterHelper(minSleepBetween float64, maxSleepBetween float64, exponentialBackoffBase float64, MaximumNumberAttempts uint, MaximumCumulativeBackoffWithoutJitter float64) time.Duration {
var cumulative time.Duration = 0
if MaximumNumberAttempts == 0 {
// unlimited
return time.Duration(MaximumCumulativeBackoffWithoutJitter) * time.Second
}
// use a one-based counter because it's easier to think about operation retry in terms of attempt numbering
for currentOperationAttempt := uint(1); currentOperationAttempt < MaximumNumberAttempts; currentOperationAttempt++ {
cumulative += time.Duration(getBackoffWithoutJitterHelper(minSleepBetween, maxSleepBetween, exponentialBackoffBase, currentOperationAttempt)) * time.Second
}
return cumulative
}
//
// Functions to calculate backoff and maximum cumulative backoff for eventual consistency
//
// GetEventuallyConsistentBackoffWithoutJitter calculates the backoff without jitter for the attempt, given the retry policy
// and dealing with eventually consistent effects. The result is then multiplied by backoffScalingFactor.
func GetEventuallyConsistentBackoffWithoutJitter(policy RetryPolicy, attempt uint, backoffScalingFactor float64) time.Duration {
return time.Duration(getEventuallyConsistentBackoffWithoutJitterHelper(policy.MinSleepBetween, policy.MaxSleepBetween, policy.ExponentialBackoffBase, attempt, backoffScalingFactor,
func(minSleepBetween float64, maxSleepBetween float64, exponentialBackoffBase float64, attempt uint) float64 {
rp := policy.NonEventuallyConsistentPolicy
return getBackoffWithoutJitterHelper(rp.MinSleepBetween, rp.MaxSleepBetween, rp.ExponentialBackoffBase, attempt)
})*1000) * time.Millisecond
}
// getEventuallyConsistentBackoffWithoutJitterHelper calculates the backoff without jitter for the attempt, given the loose retry policy values,
// and dealing with eventually consistent effects. The result is then multiplied by backoffScalingFactor.
func getEventuallyConsistentBackoffWithoutJitterHelper(minSleepBetween float64, maxSleepBetween float64, exponentialBackoffBase float64, attempt uint, backoffScalingFactor float64,
defaultBackoffWithoutJitterHelper func(minSleepBetween float64, maxSleepBetween float64, exponentialBackoffBase float64, attempt uint) float64) float64 {
var sleepTime = math.Pow(exponentialBackoffBase, float64(attempt-1))
if sleepTime < minSleepBetween {
sleepTime = minSleepBetween
}
if sleepTime > maxSleepBetween {
sleepTime = maxSleepBetween
}
sleepTime = sleepTime * backoffScalingFactor
defaultSleepTime := defaultBackoffWithoutJitterHelper(minSleepBetween, maxSleepBetween, exponentialBackoffBase, attempt)
if defaultSleepTime > sleepTime {
sleepTime = defaultSleepTime
}
return sleepTime
}
// GetMaximumCumulativeEventuallyConsistentBackoffWithoutJitter calculates the maximum backoff without jitter, according to the retry
// policy and taking eventually consistent effects into account, if every retry attempt is made.
func GetMaximumCumulativeEventuallyConsistentBackoffWithoutJitter(policy RetryPolicy) time.Duration {
return getMaximumCumulativeEventuallyConsistentBackoffWithoutJitterHelper(policy.MinSleepBetween, policy.MaxSleepBetween, policy.ExponentialBackoffBase,
policy.MaximumNumberAttempts, policy.MaximumCumulativeBackoffWithoutJitter,
func(minSleepBetween float64, maxSleepBetween float64, exponentialBackoffBase float64, attempt uint) float64 {
rp := policy.NonEventuallyConsistentPolicy
return getBackoffWithoutJitterHelper(rp.MinSleepBetween, rp.MaxSleepBetween, rp.ExponentialBackoffBase, attempt)
})
}
func getMaximumCumulativeEventuallyConsistentBackoffWithoutJitterHelper(minSleepBetween float64, maxSleepBetween float64, exponentialBackoffBase float64, MaximumNumberAttempts uint,
MaximumCumulativeBackoffWithoutJitter float64,
defaultBackoffWithoutJitterHelper func(minSleepBetween float64, maxSleepBetween float64, exponentialBackoffBase float64, attempt uint) float64) time.Duration {
if MaximumNumberAttempts == 0 {
// unlimited
return time.Duration(MaximumCumulativeBackoffWithoutJitter) * time.Second
}
var cumulative time.Duration = 0
// use a one-based counter because it's easier to think about operation retry in terms of attempt numbering
for currentOperationAttempt := uint(1); currentOperationAttempt < MaximumNumberAttempts; currentOperationAttempt++ {
cumulative += time.Duration(getEventuallyConsistentBackoffWithoutJitterHelper(minSleepBetween, maxSleepBetween, exponentialBackoffBase, currentOperationAttempt, 1.0, defaultBackoffWithoutJitterHelper)*1000) * time.Millisecond
}
return cumulative
}
func returnSamePolicy(policy RetryPolicy) (RetryPolicy, *time.Time, float64) {
// we're returning the end of window time nonetheless, even though the default non-eventual consistency (EC)
// retry policy doesn't use it; this is useful in case developers wants to write an EC-aware retry policy
// on their own
eowt := EcContext.GetEndOfWindow()
return policy, eowt, 1.0
}
// NoRetryPolicy is a helper method that assembles and returns a return policy that indicates an operation should
// never be retried (the operation is performed exactly once).
func NoRetryPolicy() RetryPolicy {
dontRetryOperation := func(OCIOperationResponse) bool { return false }
zeroNextDuration := func(OCIOperationResponse) time.Duration { return 0 * time.Second }
return newRetryPolicyWithOptionsNoDefault(
WithMaximumNumberAttempts(1),
WithShouldRetryOperation(dontRetryOperation),
WithNextDuration(zeroNextDuration),
withMinSleepBetween(0.0*time.Second),
withMaxSleepBetween(0.0*time.Second),
withExponentialBackoffBase(0.0),
withDeterminePolicyToUse(returnSamePolicy),
withNonEventuallyConsistentPolicy(nil))
}
// DefaultShouldRetryOperation is the function that should be used for RetryPolicy.ShouldRetryOperation when
// not taking eventual consistency into account.
func DefaultShouldRetryOperation(r OCIOperationResponse) bool {
if r.Error == nil && 199 < r.Response.HTTPResponse().StatusCode && r.Response.HTTPResponse().StatusCode < 300 {
// success
return false
}
return IsErrorRetryableByDefault(r.Error)
}
// DefaultRetryPolicy is a helper method that assembles and returns a return policy that is defined to be a default one
// The default retry policy will retry on (409, IncorrectState), (429, TooManyRequests) and any 5XX errors except (501, MethodNotImplemented)
// The default retry behavior is using exponential backoff with jitter, the maximum wait time is 30s plus 1s jitter
// The maximum cumulative backoff after all 8 attempts have been made is about 1.5 minutes.
// It will also retry on errors affected by eventual consistency.
// The eventual consistency retry behavior is using exponential backoff with jitter, the maximum wait time is 45s plus 1s jitter
// Under eventual consistency, the maximum cumulative backoff after all 9 attempts have been made is about 4 minutes.
func DefaultRetryPolicy() RetryPolicy {
return NewRetryPolicyWithOptions(
ReplaceWithValuesFromRetryPolicy(DefaultRetryPolicyWithoutEventualConsistency()),
WithEventualConsistency())
}
// DefaultRetryPolicyWithoutEventualConsistency is a helper method that assembles and returns a return policy that is defined to be a default one
// The default retry policy will retry on (409, IncorrectState), (429, TooManyRequests) and any 5XX errors except (501, MethodNotImplemented)
// It will not retry on errors affected by eventual consistency.
// The default retry behavior is using exponential backoff with jitter, the maximum wait time is 30s plus 1s jitter
func DefaultRetryPolicyWithoutEventualConsistency() RetryPolicy {
exponentialBackoffWithJitter := func(r OCIOperationResponse) time.Duration {
sleepTime := getBackoffWithoutJitterHelper(defaultMinSleepBetween, defaultMaxSleepBetween, defaultExponentialBackoffBase, r.AttemptNumber)
nextDuration := time.Duration(1000.0*(sleepTime+rand.Float64())) * time.Millisecond
return nextDuration
}
return newRetryPolicyWithOptionsNoDefault(
WithMaximumNumberAttempts(defaultMaximumNumberAttempts),
WithShouldRetryOperation(DefaultShouldRetryOperation),
WithNextDuration(exponentialBackoffWithJitter),
withMinSleepBetween(defaultMinSleepBetween*time.Second),
withMaxSleepBetween(defaultMaxSleepBetween*time.Second),
withExponentialBackoffBase(defaultExponentialBackoffBase),
withDeterminePolicyToUse(returnSamePolicy),
withNonEventuallyConsistentPolicy(nil))
}
// EventuallyConsistentShouldRetryOperation is the function that should be used for RetryPolicy.ShouldRetryOperation when
// taking eventual consistency into account
func EventuallyConsistentShouldRetryOperation(r OCIOperationResponse) bool {
if r.Error == nil && 199 < r.Response.HTTPResponse().StatusCode && r.Response.HTTPResponse().StatusCode < 300 {
// success
Debugln(fmt.Sprintf("EC.ShouldRetryOperation, status = %v, 2xx, returning false", r.Response.HTTPResponse().StatusCode))
return false
}
if IsErrorRetryableByDefault(r.Error) {
return true
}
// not retryable by default
if _, ok := IsServiceError(r.Error); ok {
now := EcContext.timeNowProvider()
if r.EndOfWindowTime == nil || r.EndOfWindowTime.Before(now) {
// either no eventually consistent effects, or they have disappeared by now
Debugln(fmt.Sprintf("EC.ShouldRetryOperation, no EC or in the past, returning false: endOfWindowTime = %v, now = %v", r.EndOfWindowTime, now))
return false
}
// there were eventually consistent effects present at the time of the first request
// and they could still affect the retries
if IsErrorAffectedByEventualConsistency(r.Error) {
// and it's one of the three affected error codes
Debugln(fmt.Sprintf("EC.ShouldRetryOperation, affected by EC, EC is present: endOfWindowTime = %v, now = %v", r.EndOfWindowTime, now))
return true
}
return false
}
return false
}
// EventuallyConsistentRetryPolicy is a helper method that assembles and returns a return policy that is defined to be a default one
// plus dealing with errors affected by eventual consistency.
// The default retry behavior is using exponential backoff with jitter, the maximum wait time is 45s plus 1s jitter
func EventuallyConsistentRetryPolicy(nonEventuallyConsistentPolicy RetryPolicy) RetryPolicy {
if nonEventuallyConsistentPolicy.NonEventuallyConsistentPolicy != nil {
// already deals with eventual consistency
return nonEventuallyConsistentPolicy
}
exponentialBackoffWithJitter := func(r OCIOperationResponse) time.Duration {
sleepTime := getEventuallyConsistentBackoffWithoutJitterHelper(ecMinSleepBetween, ecMaxSleepBetween, ecExponentialBackoffBase, r.AttemptNumber, r.BackoffScalingFactor,
func(minSleepBetween float64, maxSleepBetween float64, exponentialBackoffBase float64, attempt uint) float64 {
rp := nonEventuallyConsistentPolicy
return getBackoffWithoutJitterHelper(rp.MinSleepBetween, rp.MaxSleepBetween, rp.ExponentialBackoffBase, attempt)
})
nextDuration := time.Duration(1000.0*(sleepTime+rand.Float64())) * time.Millisecond
Debugln(fmt.Sprintf("EventuallyConsistentRetryPolicy.NextDuration for attempt %v: sleepTime = %.1fs, nextDuration = %v", r.AttemptNumber, sleepTime, nextDuration))
return nextDuration
}
returnModifiedPolicy := func(policy RetryPolicy) (RetryPolicy, *time.Time, float64) { return determinePolicyToUse(policy) }
nonEventuallyConsistentPolicyCopy := newRetryPolicyWithOptionsNoDefault(
ReplaceWithValuesFromRetryPolicy(nonEventuallyConsistentPolicy))
return newRetryPolicyWithOptionsNoDefault(
WithMaximumNumberAttempts(ecMaximumNumberAttempts),
WithShouldRetryOperation(EventuallyConsistentShouldRetryOperation),
WithNextDuration(exponentialBackoffWithJitter),
withMinSleepBetween(ecMinSleepBetween*time.Second),
withMaxSleepBetween(ecMaxSleepBetween*time.Second),
withExponentialBackoffBase(ecExponentialBackoffBase),
withDeterminePolicyToUse(returnModifiedPolicy),
withNonEventuallyConsistentPolicy(&nonEventuallyConsistentPolicyCopy))
}
// NewRetryPolicy is a helper method for assembling a Retry Policy object. It does not handle eventual consistency, so as to not break existing code.
// If you want to handle eventual consistency, the simplest way to do that is to replace the code
//
// NewRetryPolicy(a, r, n)
//
// with the code
//
// NewRetryPolicyWithOptions(
// WithMaximumNumberAttempts(a),
// WithFixedBackoff(fb) // fb is the fixed backoff duration
// WithShouldRetryOperation(r))
//
// or
//
// NewRetryPolicyWithOptions(
// WithMaximumNumberAttempts(a),
// WithExponentialBackoff(mb, e) // mb is the maximum backoff duration, and e is the base for exponential backoff, e.g. 2.0
// WithShouldRetryOperation(r))
//
// or, if a == 0 (the maximum number of attempts is unlimited)
//
// NewRetryPolicyWithEventualConsistencyUnlimitedAttempts(a, r, n, mcb) // mcb is the maximum cumulative backoff duration without jitter
func NewRetryPolicy(attempts uint, retryOperation func(OCIOperationResponse) bool, nextDuration func(OCIOperationResponse) time.Duration) RetryPolicy {
return NewRetryPolicyWithOptions(
ReplaceWithValuesFromRetryPolicy(DefaultRetryPolicyWithoutEventualConsistency()),
WithMaximumNumberAttempts(attempts),
WithShouldRetryOperation(retryOperation),
WithNextDuration(nextDuration),
)
}
// NewRetryPolicyWithEventualConsistencyUnlimitedAttempts is a helper method for assembling a Retry Policy object.
// It does handle eventual consistency, but other than that, it is very similar to NewRetryPolicy.
// NewRetryPolicyWithEventualConsistency does not support limited attempts, use NewRetryPolicyWithEventualConsistency instead.
func NewRetryPolicyWithEventualConsistencyUnlimitedAttempts(attempts uint, retryOperation func(OCIOperationResponse) bool, nextDuration func(OCIOperationResponse) time.Duration,
maximumCumulativeBackoffWithoutJitter time.Duration) (*RetryPolicy, error) {
if attempts != 0 {
return nil, fmt.Errorf("NewRetryPolicyWithEventualConsistencyUnlimitedAttempts cannot be used with attempts != 0 (limited attempts), use NewRetryPolicyWithEventualConsistency instead")
}
result := NewRetryPolicyWithOptions(
ReplaceWithValuesFromRetryPolicy(DefaultRetryPolicyWithoutEventualConsistency()),
WithUnlimitedAttempts(maximumCumulativeBackoffWithoutJitter),
WithShouldRetryOperation(retryOperation),
WithNextDuration(nextDuration),
)
return &result, nil
}
// NewRetryPolicyWithOptions is a helper method for assembling a Retry Policy object.
// It starts out with the values returned by DefaultRetryPolicy() and does handle eventual consistency,
// unless you replace all options set using ReplaceWithValuesFromRetryPolicy(DefaultRetryPolicyWithoutEventualConsistency()).
func NewRetryPolicyWithOptions(opts ...RetryPolicyOption) RetryPolicy {
rp := &RetryPolicy{}
// start with the default retry policy
ReplaceWithValuesFromRetryPolicy(DefaultRetryPolicyWithoutEventualConsistency())(rp)
WithEventualConsistency()(rp)
// then allow changing values
for _, opt := range opts {
opt(rp)
}
if rp.DeterminePolicyToUse == nil {
rp.DeterminePolicyToUse = returnSamePolicy
}
return *rp
}
// newRetryPolicyWithOptionsNoDefault is a helper method for assembling a Retry Policy object.
// Contrary to newRetryPolicyWithOptions, it does not start out with the values returned by
// DefaultRetryPolicy().
func newRetryPolicyWithOptionsNoDefault(opts ...RetryPolicyOption) RetryPolicy {
rp := &RetryPolicy{}
// then allow changing values
for _, opt := range opts {
opt(rp)
}
if rp.DeterminePolicyToUse == nil {
rp.DeterminePolicyToUse = returnSamePolicy
}
return *rp
}
// WithMaximumNumberAttempts is the option for NewRetryPolicyWithOptions that sets the maximum number of attempts.
func WithMaximumNumberAttempts(attempts uint) RetryPolicyOption {
// this is the RetryPolicyOption function type
return func(rp *RetryPolicy) {
rp.MaximumNumberAttempts = attempts
}
}
// WithUnlimitedAttempts is the option for NewRetryPolicyWithOptions that sets unlimited number of attempts,
// but it needs to set a MaximumCumulativeBackoffWithoutJitter duration.
// If you use WithUnlimitedAttempts, you should set your own NextDuration function using WithNextDuration.
func WithUnlimitedAttempts(maximumCumulativeBackoffWithoutJitter time.Duration) RetryPolicyOption {
// this is the RetryPolicyOption function type
return func(rp *RetryPolicy) {
rp.MaximumNumberAttempts = 0
rp.MaximumCumulativeBackoffWithoutJitter = float64(maximumCumulativeBackoffWithoutJitter / time.Second)
}
}
// WithShouldRetryOperation is the option for NewRetryPolicyWithOptions that sets the function that checks
// whether retries should be performed.
func WithShouldRetryOperation(retryOperation func(OCIOperationResponse) bool) RetryPolicyOption {
// this is the RetryPolicyOption function type
return func(rp *RetryPolicy) {
rp.ShouldRetryOperation = retryOperation
}
}
// WithNextDuration is the option for NewRetryPolicyWithOptions that sets the function for computing the next
// backoff duration.
// It is preferred to use WithFixedBackoff or WithExponentialBackoff instead.
func WithNextDuration(nextDuration func(OCIOperationResponse) time.Duration) RetryPolicyOption {
// this is the RetryPolicyOption function type
return func(rp *RetryPolicy) {
rp.NextDuration = nextDuration
}
}
// withMinSleepBetween is the option for NewRetryPolicyWithOptions that sets the minimum backoff duration.
func withMinSleepBetween(minSleepBetween time.Duration) RetryPolicyOption {
// this is the RetryPolicyOption function type
return func(rp *RetryPolicy) {
rp.MinSleepBetween = float64(minSleepBetween / time.Second)
}
}
// withMaxsSleepBetween is the option for NewRetryPolicyWithOptions that sets the maximum backoff duration.
func withMaxSleepBetween(maxSleepBetween time.Duration) RetryPolicyOption {
// this is the RetryPolicyOption function type
return func(rp *RetryPolicy) {
rp.MaxSleepBetween = float64(maxSleepBetween / time.Second)
}
}
// withExponentialBackoffBase is the option for NewRetryPolicyWithOptions that sets the base for the
// exponential backoff
func withExponentialBackoffBase(base float64) RetryPolicyOption {
// this is the RetryPolicyOption function type
return func(rp *RetryPolicy) {
rp.ExponentialBackoffBase = base
}
}
// withDeterminePolicyToUse is the option for NewRetryPolicyWithOptions that sets the function that
// determines which polich should be used and if eventual consistency should be considered
func withDeterminePolicyToUse(determinePolicyToUse func(policy RetryPolicy) (RetryPolicy, *time.Time, float64)) RetryPolicyOption {
// this is the RetryPolicyOption function type
return func(rp *RetryPolicy) {
rp.DeterminePolicyToUse = determinePolicyToUse
}
}
// withNonEventuallyConsistentPolicy is the option for NewRetryPolicyWithOptions that sets the fallback
// strategy if eventual consistency should not be considered
func withNonEventuallyConsistentPolicy(nonEventuallyConsistentPolicy *RetryPolicy) RetryPolicyOption {
// this is the RetryPolicyOption function type
return func(rp *RetryPolicy) {
// we want a non-EC policy for NonEventuallyConsistentPolicy; make sure that NonEventuallyConsistentPolicy is nil
for nonEventuallyConsistentPolicy != nil && nonEventuallyConsistentPolicy.NonEventuallyConsistentPolicy != nil {
nonEventuallyConsistentPolicy = nonEventuallyConsistentPolicy.NonEventuallyConsistentPolicy
}
rp.NonEventuallyConsistentPolicy = nonEventuallyConsistentPolicy
}
}
// WithExponentialBackoff is an option for NewRetryPolicyWithOptions that sets the exponential backoff base,
// minimum and maximum sleep between attempts, and next duration function.
// Therefore, WithExponentialBackoff is a combination of WithNextDuration, withMinSleepBetween, withMaxSleepBetween,
// and withExponentialBackoffBase.
func WithExponentialBackoff(newMaxSleepBetween time.Duration, newExponentialBackoffBase float64) RetryPolicyOption {
exponentialBackoffWithJitter := func(r OCIOperationResponse) time.Duration {
sleepTime := getBackoffWithoutJitterHelper(defaultMinSleepBetween, newMaxSleepBetween.Seconds(), newExponentialBackoffBase, r.AttemptNumber)
nextDuration := time.Duration(1000.0*(sleepTime+rand.Float64())) * time.Millisecond
Debugln(fmt.Sprintf("NextDuration for attempt %v: sleepTime = %.1fs, nextDuration = %v", r.AttemptNumber, sleepTime, nextDuration))
return nextDuration
}
// this is the RetryPolicyOption function type
return func(rp *RetryPolicy) {
withMinSleepBetween(0)(rp)
withMaxSleepBetween(newMaxSleepBetween)(rp)
withExponentialBackoffBase(newExponentialBackoffBase)(rp)
WithNextDuration(exponentialBackoffWithJitter)(rp)
}
}
// WithFixedBackoff is an option for NewRetryPolicyWithOptions that sets the backoff to always be exactly the same value. There is no jitter either.
// Therefore, WithFixedBackoff is a combination of WithNextDuration, withMinSleepBetween, withMaxSleepBetween, and withExponentialBackoffBase.
func WithFixedBackoff(newSleepBetween time.Duration) RetryPolicyOption {
fixedBackoffWithoutJitter := func(r OCIOperationResponse) time.Duration {
nextDuration := newSleepBetween
Debugln(fmt.Sprintf("NextDuration for attempt %v: nextDuration = %v", r.AttemptNumber, nextDuration))
return nextDuration
}
// this is the RetryPolicyOption function type
return func(rp *RetryPolicy) {
withMinSleepBetween(newSleepBetween)(rp)
withMaxSleepBetween(newSleepBetween)(rp)
withExponentialBackoffBase(1.0)(rp)
WithNextDuration(fixedBackoffWithoutJitter)(rp)
}
}
// WithEventualConsistency is the option for NewRetryPolicyWithOptions that enables considering eventual backoff for the policy.
func WithEventualConsistency() RetryPolicyOption {
// this is the RetryPolicyOption function type
return func(rp *RetryPolicy) {
copy := RetryPolicy{
MaximumNumberAttempts: rp.MaximumNumberAttempts,
ShouldRetryOperation: rp.ShouldRetryOperation,
NextDuration: rp.NextDuration,
MinSleepBetween: rp.MinSleepBetween,
MaxSleepBetween: rp.MaxSleepBetween,
ExponentialBackoffBase: rp.ExponentialBackoffBase,
DeterminePolicyToUse: rp.DeterminePolicyToUse,
NonEventuallyConsistentPolicy: rp.NonEventuallyConsistentPolicy,
}
ecrp := EventuallyConsistentRetryPolicy(copy)
rp.MaximumNumberAttempts = ecrp.MaximumNumberAttempts
rp.ShouldRetryOperation = ecrp.ShouldRetryOperation
rp.NextDuration = ecrp.NextDuration
rp.MinSleepBetween = ecrp.MinSleepBetween
rp.MaxSleepBetween = ecrp.MaxSleepBetween
rp.ExponentialBackoffBase = ecrp.ExponentialBackoffBase
rp.DeterminePolicyToUse = ecrp.DeterminePolicyToUse
rp.NonEventuallyConsistentPolicy = ecrp.NonEventuallyConsistentPolicy
}
}
// WithConditionalOption is an option for NewRetryPolicyWithOptions that enables or disables another option.
func WithConditionalOption(enabled bool, otherOption RetryPolicyOption) RetryPolicyOption {
// this is the RetryPolicyOption function type
return func(rp *RetryPolicy) {
if enabled {
otherOption(rp)
}
}
}
// ReplaceWithValuesFromRetryPolicy is an option for NewRetryPolicyWithOptions that copies over all settings from another RetryPolicy
func ReplaceWithValuesFromRetryPolicy(other RetryPolicy) RetryPolicyOption {
// this is the RetryPolicyOption function type
return func(rp *RetryPolicy) {
rp.MaximumNumberAttempts = other.MaximumNumberAttempts
rp.ShouldRetryOperation = other.ShouldRetryOperation
rp.NextDuration = other.NextDuration
rp.MinSleepBetween = other.MinSleepBetween
rp.MaxSleepBetween = other.MaxSleepBetween
rp.ExponentialBackoffBase = other.ExponentialBackoffBase
rp.DeterminePolicyToUse = other.DeterminePolicyToUse
rp.NonEventuallyConsistentPolicy = other.NonEventuallyConsistentPolicy
rp.MaximumCumulativeBackoffWithoutJitter = other.MaximumCumulativeBackoffWithoutJitter
}
}
// shouldContinueIssuingRequests returns true if we should continue retrying a request, based on the current attempt
// number and the maximum number of attempts specified, or false otherwise.
func shouldContinueIssuingRequests(current, maximum uint) bool {
return maximum == UnlimitedNumAttemptsValue || current <= maximum
}
// RetryToken generates a retry token that must be included on any request passed to the Retry method.
func RetryToken() string {
alphanumericChars := []rune("abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")
retryToken := make([]rune, generatedRetryTokenLength)
for i := range retryToken {
retryToken[i] = alphanumericChars[rand.Intn(len(alphanumericChars))]
}
return string(retryToken)
}
func determinePolicyToUse(policy RetryPolicy) (RetryPolicy, *time.Time, float64) {
initialAttemptTime := EcContext.timeNowProvider()
var useDefaultTimingInstead = true
var endOfWindowTime = (*time.Time)(nil)
var backoffScalingFactor = 1.0
var policyToUse = policy
eowt := EcContext.GetEndOfWindow()
if eowt != nil {
// there was an eventually consistent request
if eowt.After(initialAttemptTime) {
// and the eventually consistent effects may still be present
endOfWindowTime = eowt
// if the time between now and the end of the window is less than the time we normally would retry, use the default timing
durationToEndOfWindow := endOfWindowTime.Sub(initialAttemptTime)
maxCumulativeBackoffWithoutJitter := GetMaximumCumulativeBackoffWithoutJitter(*policy.NonEventuallyConsistentPolicy)
Debugln(fmt.Sprintf("durationToEndOfWindow = %v, maxCumulativeBackoffWithoutJitter = %v", durationToEndOfWindow, maxCumulativeBackoffWithoutJitter))
if durationToEndOfWindow > maxCumulativeBackoffWithoutJitter {
// the end of the eventually consistent window is later than when default retries would end
// do not use default timing
maximumCumulativeBackoffWithoutJitter := GetMaximumCumulativeEventuallyConsistentBackoffWithoutJitter(policy)
backoffScalingFactor = float64(durationToEndOfWindow) / float64(maximumCumulativeBackoffWithoutJitter)
useDefaultTimingInstead = false
Debugln(fmt.Sprintf("Use eventually consistent timing, durationToEndOfWindow = %v, maximumCumulativeBackoffWithoutJitter = %v, backoffScalingFactor = %.2f",
durationToEndOfWindow, maximumCumulativeBackoffWithoutJitter, backoffScalingFactor))
} else {
Debugln("Use default timing, end of EC window is sooner than default retries")
}
} else {
useDefaultTimingInstead = false
policyToUse = *policy.NonEventuallyConsistentPolicy
Debugln("Use default timing and strategy, end of EC window is in the past")
}
} else {
useDefaultTimingInstead = false
policyToUse = *policy.NonEventuallyConsistentPolicy
Debugln("Use default timing and strategy, no EC window set")
}
if useDefaultTimingInstead {
// use timing from defaultRetryPolicy, but whether to retry from the policy that was passed into this request
policyToUse = NewRetryPolicyWithOptions(
ReplaceWithValuesFromRetryPolicy(*policy.NonEventuallyConsistentPolicy),
WithShouldRetryOperation(policy.ShouldRetryOperation))
}
return policyToUse, endOfWindowTime, backoffScalingFactor
}
// Retry is a package-level operation that executes the retryable request using the specified operation and retry policy.
func Retry(ctx context.Context, request OCIRetryableRequest, operation OCIOperation, policy RetryPolicy) (OCIResponse, error) {
type retrierResult struct {
response OCIResponse
err error
}
var response OCIResponse
var err error
retrierChannel := make(chan retrierResult, 1)
validated, validateError := policy.validate()
if !validated {
return nil, validateError
}
initialAttemptTime := time.Now()
go func() {
// Deal with panics more graciously
defer func() {
if r := recover(); r != nil {
stackBuffer := make([]byte, 1024)
bytesWritten := runtime.Stack(stackBuffer, false)
stack := string(stackBuffer[:bytesWritten])
error := fmt.Errorf("panicked while retrying operation. Panic was: %s\nStack: %s", r, stack)
Debugln(error)
retrierChannel <- retrierResult{nil, error}
}
}()
// if request body is binary request body and seekable, save the current position
var curPos int64 = 0
isSeekable := false
rsc, isBinaryRequest := request.BinaryRequestBody()
if rsc != nil && rsc.rc != nil {
defer rsc.rc.Close()
}
if policy.MaximumNumberAttempts != uint(1) {
if rsc.Seekable() {
isSeekable = true
curPos, _ = rsc.Seek(0, io.SeekCurrent)
}
}
// some legacy code constructing RetryPolicy instances directly may not have set DeterminePolicyToUse.
// In that case, just assume that it doesn't handle eventual consistency.
if policy.DeterminePolicyToUse == nil {
policy.DeterminePolicyToUse = returnSamePolicy
}
// this determines which policy to use, when the eventual consistency window ends, and what the backoff
// scaling factor should be
policyToUse, endOfWindowTime, backoffScalingFactor := policy.DeterminePolicyToUse(policy)
Debugln(fmt.Sprintf("Retry policy to use: %v", policyToUse))
retryStartTime := time.Now()
extraHeaders := make(map[string]string)
if policy.MaximumNumberAttempts == 1 {
extraHeaders[requestHeaderOpcClientRetries] = "false"
} else {
extraHeaders[requestHeaderOpcClientRetries] = "true"
}
// use a one-based counter because it's easier to think about operation retry in terms of attempt numbering
for currentOperationAttempt := uint(1); shouldContinueIssuingRequests(currentOperationAttempt, policyToUse.MaximumNumberAttempts); currentOperationAttempt++ {
Debugln(fmt.Sprintf("operation attempt #%v", currentOperationAttempt))
// rewind body once needed
if isSeekable {
rsc = NewOCIReadSeekCloser(rsc.rc)
rsc.Seek(curPos, io.SeekStart)
}
response, err = operation(ctx, request, rsc, extraHeaders)
operationResponse := NewOCIOperationResponseExtended(response, err, currentOperationAttempt, endOfWindowTime, backoffScalingFactor, initialAttemptTime)
if !policyToUse.ShouldRetryOperation(operationResponse) {
// we should NOT retry operation based on response and/or error => return
retrierChannel <- retrierResult{response, err}
return
}
// if the request body type is stream, requested retry but doesn't resettable, throw error and stop retrying
if isBinaryRequest && !isSeekable {
retrierChannel <- retrierResult{response, NonSeekableRequestRetryFailure{err}}
return
}
duration := policyToUse.NextDuration(operationResponse)
//The following condition is kept for backwards compatibility reasons
if deadline, ok := ctx.Deadline(); ok && EcContext.timeNowProvider().Add(duration).After(deadline) {
// we want to retry the operation, but the policy is telling us to wait for a duration that exceeds
// the specified overall deadline for the operation => instead of waiting for however long that
// time period is and then aborting, abort now and save the cycles
retrierChannel <- retrierResult{response, DeadlineExceededByBackoff}
return
}
Debugln(fmt.Sprintf("waiting %v before retrying operation", duration))
// sleep before retrying the operation
<-time.After(duration)
}
retryEndTime := time.Now()
Debugln(fmt.Sprintf("Total Latency for this API call is: %v ms", retryEndTime.Sub(retryStartTime).Milliseconds()))
retrierChannel <- retrierResult{response, err}
}()
select {
case <-ctx.Done():
return response, ctx.Err()
case result := <-retrierChannel:
return result.response, result.err
}
}

View File

@@ -0,0 +1,92 @@
// Copyright (c) 2016, 2018, 2025, Oracle and/or its affiliates. All rights reserved.
// This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license.
package common
import (
"bufio"
"bytes"
"context"
"io"
"net/http"
)
type SseReader struct {
HttpBody io.ReadCloser
eventScanner bufio.Scanner
OnClose func(r *SseReader)
}
// InvalidSSEResponseError returned in the case that a nil response body was given
// to NewSSEReader()
type InvalidSSEResponseError struct {
}
const InvalidResponseErrorMessage = "invalid response struct given to NewSSEReader"
func (e InvalidSSEResponseError) Error() string {
return InvalidResponseErrorMessage
}
// NewSSEReader returns an SSE Reader given an sse response
func NewSSEReader(response *http.Response) (*SseReader, error) {
if response == nil || response.Body == nil {
return nil, InvalidSSEResponseError{}
}
reader := &SseReader{
HttpBody: response.Body,
eventScanner: *bufio.NewScanner(response.Body),
OnClose: func(r *SseReader) { r.HttpBody.Close() }, // Default on close function, ensures body is closed after use
}
return reader, nil
}
// Take the response in bytes and trim it if necessary
func processEvent(e []byte) []byte {
e = bytes.TrimPrefix(e, []byte("data: ")) // Text/event-stream always prefixed with 'data: '
return e
}
// ReadNextEvent reads the next event in the stream, return it unmarshalled
func (r *SseReader) ReadNextEvent() (event []byte, err error) {
if r.eventScanner.Scan() {
eventBytes := r.eventScanner.Bytes()
return processEvent(eventBytes), nil
} else {
// Close out the stream since we are finished reading from it
if r.OnClose != nil {
r.OnClose(r)
}
err := r.eventScanner.Err()
if err == context.Canceled || err == nil {
err = io.EOF
}
return nil, err
}
}
// ReadAllEvents reads all events from the response stream, and processes each with given event handler
func (r *SseReader) ReadAllEvents(eventHandler func(e []byte)) error {
for {
event, err := r.ReadNextEvent()
if err != nil {
if err == io.EOF {
err = nil
}
return err
}
// Ignore empty events
if len(event) > 0 {
eventHandler(event)
}
}
}

View File

@@ -0,0 +1,156 @@
// Copyright (c) 2016, 2018, 2025, Oracle and/or its affiliates. All rights reserved.
// This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license.
package common
import (
"crypto/tls"
"crypto/x509"
"fmt"
"os"
"sync"
)
// GetTLSConfigTemplateForTransport returns the TLSConfigTemplate to used depending on whether any additional
// CA Bundle or client side certs have been configured
func GetTLSConfigTemplateForTransport() TLSConfigProvider {
certPath := os.Getenv(ociDefaultClientCertsPath)
keyPath := os.Getenv(ociDefaultClientCertsPrivateKeyPath)
caBundlePath := os.Getenv(ociDefaultCertsPath)
if certPath != "" && keyPath != "" {
return &DefaultMTLSConfigProvider{
caBundlePath: caBundlePath,
clientCertPath: certPath,
clientKeyPath: keyPath,
watchedFilesStatsMap: make(map[string]os.FileInfo),
}
}
return &DefaultTLSConfigProvider{
caBundlePath: caBundlePath,
}
}
// TLSConfigProvider is an interface the defines a function that creates a new *tls.Config.
type TLSConfigProvider interface {
NewOrDefault() (*tls.Config, error)
WatchedFilesModified() bool
}
// DefaultTLSConfigProvider is a provider that provides a TLS tls.config for the HTTPTransport
type DefaultTLSConfigProvider struct {
caBundlePath string
mux sync.Mutex
currentStat os.FileInfo
}
// NewOrDefault returns a default tls.Config which
// sets its RootCAs to be a *x509.CertPool from caBundlePath.
func (t *DefaultTLSConfigProvider) NewOrDefault() (*tls.Config, error) {
if t.caBundlePath == "" {
return &tls.Config{}, nil
}
// Keep the current Stat info from the ca bundle in a map
Debugf("Getting Initial Stats for file: %s", t.caBundlePath)
caBundleStat, err := os.Stat(t.caBundlePath)
if err != nil {
return nil, err
}
t.mux.Lock()
defer t.mux.Unlock()
t.currentStat = caBundleStat
rootCAs, err := CertPoolFrom(t.caBundlePath)
if err != nil {
return nil, err
}
return &tls.Config{
RootCAs: rootCAs,
}, nil
}
// WatchedFilesModified returns true if any files in the watchedFilesStatsMap has been modified else returns false
func (t *DefaultTLSConfigProvider) WatchedFilesModified() bool {
modified := false
if t.caBundlePath != "" {
newStat, err := os.Stat(t.caBundlePath)
if err == nil && (t.currentStat.Size() != newStat.Size() || t.currentStat.ModTime() != newStat.ModTime()) {
Logf("Modification detected in cert/ca-bundle file: %s", t.caBundlePath)
modified = true
t.mux.Lock()
defer t.mux.Unlock()
t.currentStat = newStat
}
}
return modified
}
// DefaultMTLSConfigProvider is a provider that provides a MTLS tls.config for the HTTPTransport
type DefaultMTLSConfigProvider struct {
caBundlePath string
clientCertPath string
clientKeyPath string
mux sync.Mutex
watchedFilesStatsMap map[string]os.FileInfo
}
// NewOrDefault returns a default tls.Config which sets its RootCAs
// to be a *x509.CertPool from caBundlePath and calls
// tls.LoadX509KeyPair(clientCertPath, clientKeyPath) to set mtls client certs.
func (t *DefaultMTLSConfigProvider) NewOrDefault() (*tls.Config, error) {
rootCAs, err := CertPoolFrom(t.caBundlePath)
if err != nil {
return nil, err
}
cert, err := tls.LoadX509KeyPair(t.clientCertPath, t.clientKeyPath)
if err != nil {
return nil, err
}
// Configure the initial certs file stats, error skipped because we error out before this if the files don't exist
t.mux.Lock()
defer t.mux.Unlock()
t.watchedFilesStatsMap[t.caBundlePath], _ = os.Stat(t.caBundlePath)
t.watchedFilesStatsMap[t.clientCertPath], _ = os.Stat(t.clientCertPath)
t.watchedFilesStatsMap[t.clientKeyPath], _ = os.Stat(t.clientKeyPath)
return &tls.Config{
RootCAs: rootCAs,
Certificates: []tls.Certificate{cert},
}, nil
}
// WatchedFilesModified returns true if any files in the watchedFilesStatsMap has been modified else returns false
func (t *DefaultMTLSConfigProvider) WatchedFilesModified() bool {
modified := false
t.mux.Lock()
defer t.mux.Unlock()
for k, v := range t.watchedFilesStatsMap {
if k != "" {
currentStat, err := os.Stat(k)
if err == nil && (v.Size() != currentStat.Size() || v.ModTime() != currentStat.ModTime()) {
modified = true
Logf("Modification detected in cert/ca-bundle file: %s", k)
t.watchedFilesStatsMap[k] = currentStat
}
}
}
return modified
}
// CertPoolFrom creates a new x509.CertPool from a given file.
func CertPoolFrom(caBundleFile string) (*x509.CertPool, error) {
pemCerts, err := os.ReadFile(caBundleFile)
if err != nil {
return nil, err
}
trust := x509.NewCertPool()
if !trust.AppendCertsFromPEM(pemCerts) {
return nil, fmt.Errorf("creating a new x509.CertPool from %s: no certs added", caBundleFile)
}
return trust, nil
}

View File

@@ -0,0 +1,53 @@
// Copyright (c) 2016, 2018, 2025, Oracle and/or its affiliates. All rights reserved.
// This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license.
package common
import (
"crypto/tls"
"net"
"net/http"
"time"
)
// TransportTemplateProvider defines a function that creates a new http transport
// from a given TLS client config.
type TransportTemplateProvider func(tlsClientConfig *tls.Config) (http.RoundTripper, error)
// NewOrDefault creates a new TransportTemplate
// If t is nil, then DefaultTransport is returned
func (t TransportTemplateProvider) NewOrDefault(tlsClientConfig *tls.Config) (http.RoundTripper, error) {
if t == nil {
return DefaultTransport(tlsClientConfig)
}
return t(tlsClientConfig)
}
// DefaultTransport creates a clone of http.DefaultTransport
// and applies the tlsClientConfig on top of it.
// The result is never nil, to prevent panics in client code.
// Never returns any errors, but needs to return an error
// to adhere to TransportTemplate interface.
func DefaultTransport(tlsClientConfig *tls.Config) (*http.Transport, error) {
transport := CloneHTTPDefaultTransport()
if isExpectHeaderDisabled := IsEnvVarFalse(UsingExpectHeaderEnvVar); !isExpectHeaderDisabled {
transport.Proxy = http.ProxyFromEnvironment
transport.DialContext = (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext
transport.ForceAttemptHTTP2 = true
transport.MaxIdleConns = 100
transport.IdleConnTimeout = 90 * time.Second
transport.TLSHandshakeTimeout = 10 * time.Second
transport.ExpectContinueTimeout = 3 * time.Second
}
transport.TLSClientConfig = tlsClientConfig
return transport, nil
}
// CloneHTTPDefaultTransport returns a clone of http.DefaultTransport.
func CloneHTTPDefaultTransport() *http.Transport {
return http.DefaultTransport.(*http.Transport).Clone()
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) 2016, 2018, 2020, Oracle and/or its affiliates. All rights reserved.
// This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license.
// Code generated by go generate; DO NOT EDIT
package common
import (
"bytes"
"fmt"
"sync"
)
const (
major = "65"
minor = "95"
patch = "2"
tag = ""
)
var once sync.Once
var version string
// Version returns semantic version of the sdk
func Version() string {
once.Do(func() {
ver := fmt.Sprintf("%s.%s.%s", major, minor, patch)
verBuilder := bytes.NewBufferString(ver)
if tag != "" && tag != "-" {
_, err := verBuilder.WriteString(tag)
if err != nil {
verBuilder = bytes.NewBufferString(ver)
}
}
version = verBuilder.String()
})
return version
}