From 11708073e58be0d63f7ef641eb0235f7146b165f Mon Sep 17 00:00:00 2001 From: nanhe Date: Tue, 5 Nov 2024 12:17:55 +0800 Subject: [PATCH] feat: add imds and external uri in default chain && resolve credentials timeout --- credentials/credential.go | 72 +++++--- credentials/credential_test.go | 6 +- credentials/internal/http/http.go | 7 + credentials/internal/http/http_test.go | 18 ++ credentials/providers/cli_profile.go | 3 +- credentials/providers/cli_profile_test.go | 9 +- credentials/providers/default.go | 14 +- credentials/providers/default_test.go | 42 ++++- credentials/providers/ecs_ram_role.go | 85 +++++++-- credentials/providers/ecs_ram_role_test.go | 54 ++++++ credentials/providers/oidc.go | 15 +- credentials/providers/oidc_test.go | 27 ++- credentials/providers/ram_role_arn.go | 23 ++- credentials/providers/ram_role_arn_test.go | 50 +++++- credentials/providers/uri.go | 152 ++++++++++++++++ credentials/providers/uri_test.go | 199 +++++++++++++++++++++ 16 files changed, 702 insertions(+), 74 deletions(-) create mode 100644 credentials/providers/uri.go create mode 100644 credentials/providers/uri_test.go diff --git a/credentials/credential.go b/credentials/credential.go index a9563aa..ad80fc5 100644 --- a/credentials/credential.go +++ b/credentials/credential.go @@ -39,32 +39,50 @@ type Credential interface { // Config is important when call NewCredential type Config struct { - Type *string `json:"type"` - AccessKeyId *string `json:"access_key_id"` - AccessKeySecret *string `json:"access_key_secret"` - OIDCProviderArn *string `json:"oidc_provider_arn"` - OIDCTokenFilePath *string `json:"oidc_token"` - RoleArn *string `json:"role_arn"` - RoleSessionName *string `json:"role_session_name"` - PublicKeyId *string `json:"public_key_id"` - RoleName *string `json:"role_name"` - EnableIMDSv2 *bool `json:"enable_imds_v2"` - DisableIMDSv1 *bool `json:"disable_imds_v1"` - MetadataTokenDuration *int `json:"metadata_token_duration"` - SessionExpiration *int `json:"session_expiration"` - PrivateKeyFile *string `json:"private_key_file"` - BearerToken *string `json:"bearer_token"` - SecurityToken *string `json:"security_token"` - RoleSessionExpiration *int `json:"role_session_expiration"` - Policy *string `json:"policy"` - Host *string `json:"host"` - Timeout *int `json:"timeout"` - ConnectTimeout *int `json:"connect_timeout"` - Proxy *string `json:"proxy"` - InAdvanceScale *float64 `json:"inAdvanceScale"` - Url *string `json:"url"` - STSEndpoint *string `json:"sts_endpoint"` - ExternalId *string `json:"external_id"` + // Credential type, including access_key, sts, bearer, ecs_ram_role, ram_role_arn, rsa_key_pair, oidc_role_arn, credentials_uri + Type *string `json:"type"` + AccessKeyId *string `json:"access_key_id"` + AccessKeySecret *string `json:"access_key_secret"` + SecurityToken *string `json:"security_token"` + BearerToken *string `json:"bearer_token"` + + // Used when the type is ram_role_arn or oidc_role_arn + OIDCProviderArn *string `json:"oidc_provider_arn"` + OIDCTokenFilePath *string `json:"oidc_token"` + RoleArn *string `json:"role_arn"` + RoleSessionName *string `json:"role_session_name"` + RoleSessionExpiration *int `json:"role_session_expiration"` + Policy *string `json:"policy"` + ExternalId *string `json:"external_id"` + STSEndpoint *string `json:"sts_endpoint"` + + // Used when the type is ecs_ram_role + RoleName *string `json:"role_name"` + // Deprecated + EnableIMDSv2 *bool `json:"enable_imds_v2"` + DisableIMDSv1 *bool `json:"disable_imds_v1"` + // Deprecated + MetadataTokenDuration *int `json:"metadata_token_duration"` + + // Used when the type is credentials_uri + Url *string `json:"url"` + + // Deprecated + // Used when the type is rsa_key_pair + SessionExpiration *int `json:"session_expiration"` + PublicKeyId *string `json:"public_key_id"` + PrivateKeyFile *string `json:"private_key_file"` + Host *string `json:"host"` + + // Read timeout, in milliseconds. + // The default value for ecs_ram_role is 1000ms, the default value for ram_role_arn is 5000ms, and the default value for oidc_role_arn is 5000ms. + Timeout *int `json:"timeout"` + // Connection timeout, in milliseconds. + // The default value for ecs_ram_role is 1000ms, the default value for ram_role_arn is 10000ms, and the default value for oidc_role_arn is 10000ms. + ConnectTimeout *int `json:"connect_timeout"` + + Proxy *string `json:"proxy"` + InAdvanceScale *float64 `json:"inAdvanceScale"` } func (s Config) String() string { @@ -343,7 +361,7 @@ func NewCredential(config *Config) (credential Credential, err error) { } credential = newBearerTokenCredential(tea.StringValue(config.BearerToken)) default: - err = errors.New("invalid type option, support: access_key, sts, ecs_ram_role, ram_role_arn, rsa_key_pair") + err = errors.New("invalid type option, support: access_key, sts, bearer, ecs_ram_role, ram_role_arn, rsa_key_pair, oidc_role_arn, credentials_uri") return } return credential, nil diff --git a/credentials/credential_test.go b/credentials/credential_test.go index b28e155..62c9faf 100644 --- a/credentials/credential_test.go +++ b/credentials/credential_test.go @@ -15,8 +15,8 @@ this is privatekey` func TestConfig(t *testing.T) { config := new(Config) - assert.Equal(t, "{\n \"type\": null,\n \"access_key_id\": null,\n \"access_key_secret\": null,\n \"oidc_provider_arn\": null,\n \"oidc_token\": null,\n \"role_arn\": null,\n \"role_session_name\": null,\n \"public_key_id\": null,\n \"role_name\": null,\n \"enable_imds_v2\": null,\n \"disable_imds_v1\": null,\n \"metadata_token_duration\": null,\n \"session_expiration\": null,\n \"private_key_file\": null,\n \"bearer_token\": null,\n \"security_token\": null,\n \"role_session_expiration\": null,\n \"policy\": null,\n \"host\": null,\n \"timeout\": null,\n \"connect_timeout\": null,\n \"proxy\": null,\n \"inAdvanceScale\": null,\n \"url\": null,\n \"sts_endpoint\": null,\n \"external_id\": null\n}", config.String()) - assert.Equal(t, "{\n \"type\": null,\n \"access_key_id\": null,\n \"access_key_secret\": null,\n \"oidc_provider_arn\": null,\n \"oidc_token\": null,\n \"role_arn\": null,\n \"role_session_name\": null,\n \"public_key_id\": null,\n \"role_name\": null,\n \"enable_imds_v2\": null,\n \"disable_imds_v1\": null,\n \"metadata_token_duration\": null,\n \"session_expiration\": null,\n \"private_key_file\": null,\n \"bearer_token\": null,\n \"security_token\": null,\n \"role_session_expiration\": null,\n \"policy\": null,\n \"host\": null,\n \"timeout\": null,\n \"connect_timeout\": null,\n \"proxy\": null,\n \"inAdvanceScale\": null,\n \"url\": null,\n \"sts_endpoint\": null,\n \"external_id\": null\n}", config.GoString()) + assert.Equal(t, "{\n \"type\": null,\n \"access_key_id\": null,\n \"access_key_secret\": null,\n \"security_token\": null,\n \"bearer_token\": null,\n \"oidc_provider_arn\": null,\n \"oidc_token\": null,\n \"role_arn\": null,\n \"role_session_name\": null,\n \"role_session_expiration\": null,\n \"policy\": null,\n \"external_id\": null,\n \"sts_endpoint\": null,\n \"role_name\": null,\n \"enable_imds_v2\": null,\n \"disable_imds_v1\": null,\n \"metadata_token_duration\": null,\n \"url\": null,\n \"session_expiration\": null,\n \"public_key_id\": null,\n \"private_key_file\": null,\n \"host\": null,\n \"timeout\": null,\n \"connect_timeout\": null,\n \"proxy\": null,\n \"inAdvanceScale\": null\n}", config.String()) + assert.Equal(t, "{\n \"type\": null,\n \"access_key_id\": null,\n \"access_key_secret\": null,\n \"security_token\": null,\n \"bearer_token\": null,\n \"oidc_provider_arn\": null,\n \"oidc_token\": null,\n \"role_arn\": null,\n \"role_session_name\": null,\n \"role_session_expiration\": null,\n \"policy\": null,\n \"external_id\": null,\n \"sts_endpoint\": null,\n \"role_name\": null,\n \"enable_imds_v2\": null,\n \"disable_imds_v1\": null,\n \"metadata_token_duration\": null,\n \"url\": null,\n \"session_expiration\": null,\n \"public_key_id\": null,\n \"private_key_file\": null,\n \"host\": null,\n \"timeout\": null,\n \"connect_timeout\": null,\n \"proxy\": null,\n \"inAdvanceScale\": null\n}", config.GoString()) config.SetSTSEndpoint("sts.cn-hangzhou.aliyuncs.com") assert.Equal(t, "sts.cn-hangzhou.aliyuncs.com", *config.STSEndpoint) @@ -309,7 +309,7 @@ func TestNewCredentialWithInvalidType(t *testing.T) { config.SetType("sdk") cred, err := NewCredential(config) assert.NotNil(t, err) - assert.Equal(t, "invalid type option, support: access_key, sts, ecs_ram_role, ram_role_arn, rsa_key_pair", err.Error()) + assert.Equal(t, "invalid type option, support: access_key, sts, bearer, ecs_ram_role, ram_role_arn, rsa_key_pair, oidc_role_arn, credentials_uri", err.Error()) assert.Nil(t, cred) } diff --git a/credentials/internal/http/http.go b/credentials/internal/http/http.go index 4c97122..bc5dfd5 100644 --- a/credentials/internal/http/http.go +++ b/credentials/internal/http/http.go @@ -17,6 +17,7 @@ import ( type Request struct { Method string // http request method + URL string // http url Protocol string // http or https Host string // http host ReadTimeout time.Duration @@ -31,6 +32,9 @@ type Request struct { func (req *Request) BuildRequestURL() string { httpUrl := fmt.Sprintf("%s://%s%s", req.Protocol, req.Host, req.Path) + if req.URL != "" { + httpUrl = req.URL + } querystring := utils.GetURLFormedMap(req.Queries) if querystring != "" { @@ -60,6 +64,9 @@ func Do(req *Request) (res *Response, err error) { querystring := utils.GetURLFormedMap(req.Queries) // do request httpUrl := fmt.Sprintf("%s://%s%s?%s", req.Protocol, req.Host, req.Path, querystring) + if req.URL != "" { + httpUrl = req.URL + } var body io.Reader if req.Method == "GET" { diff --git a/credentials/internal/http/http_test.go b/credentials/internal/http/http_test.go index ad2d067..e4e71d0 100644 --- a/credentials/internal/http/http_test.go +++ b/credentials/internal/http/http_test.go @@ -19,6 +19,14 @@ func TestRequest(t *testing.T) { Path: "/", } assert.Equal(t, "GET http://www.aliyun.com/", req.BuildRequestURL()) + + req = &Request{ + Method: "GET", + URL: "http://www.aliyun.com", + Path: "/", + } + assert.Equal(t, "GET http://www.aliyun.com", req.BuildRequestURL()) + // With query req = &Request{ Method: "GET", @@ -44,6 +52,16 @@ func TestDoGet(t *testing.T) { assert.NotNil(t, res) assert.Equal(t, 200, res.StatusCode) assert.Equal(t, "text/html; charset=utf-8", res.Headers["Content-Type"]) + + req = &Request{ + Method: "GET", + URL: "http://www.aliyun.com", + } + res, err = Do(req) + assert.Nil(t, err) + assert.NotNil(t, res) + assert.Equal(t, 200, res.StatusCode) + assert.Equal(t, "text/html; charset=utf-8", res.Headers["Content-Type"]) } func TestDoPost(t *testing.T) { diff --git a/credentials/providers/cli_profile.go b/credentials/providers/cli_profile.go index 6a9463e..cdd240e 100644 --- a/credentials/providers/cli_profile.go +++ b/credentials/providers/cli_profile.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "os" "path" + "strings" "github.com/aliyun/credentials-go/credentials/internal/utils" ) @@ -34,7 +35,7 @@ func (b *CLIProfileCredentialsProviderBuilder) Build() (provider *CLIProfileCred b.provider.profileName = os.Getenv("ALIBABA_CLOUD_PROFILE") } - if os.Getenv("ALIBABA_CLOUD_CLI_PROFILE_DISABLED") == "true" { + if strings.ToLower(os.Getenv("ALIBABA_CLOUD_CLI_PROFILE_DISABLED")) == "true" { err = errors.New("the CLI profile is disabled") return } diff --git a/credentials/providers/cli_profile_test.go b/credentials/providers/cli_profile_test.go index 2f17b18..1c183dc 100644 --- a/credentials/providers/cli_profile_test.go +++ b/credentials/providers/cli_profile_test.go @@ -11,7 +11,7 @@ import ( ) func TestCLIProfileCredentialsProvider(t *testing.T) { - rollback := utils.Memory("ALIBABA_CLOUD_PROFILE") + rollback := utils.Memory("ALIBABA_CLOUD_PROFILE", "ALIBABA_CLOUD_CLI_PROFILE_DISABLED") defer rollback() b, err := NewCLIProfileCredentialsProviderBuilder(). @@ -31,6 +31,13 @@ func TestCLIProfileCredentialsProvider(t *testing.T) { Build() assert.Nil(t, err) assert.Equal(t, "profilename", b.profileName) + + os.Setenv("ALIBABA_CLOUD_CLI_PROFILE_DISABLED", "True") + _, err = NewCLIProfileCredentialsProviderBuilder(). + WithProfileName("profilename"). + Build() + assert.Equal(t, "the CLI profile is disabled", err.Error()) + } func Test_configuration(t *testing.T) { diff --git a/credentials/providers/default.go b/credentials/providers/default.go index 17b614d..9743676 100644 --- a/credentials/providers/default.go +++ b/credentials/providers/default.go @@ -39,15 +39,19 @@ func NewDefaultCredentialsProvider() (provider *DefaultCredentialsProvider) { } // Add IMDS - if os.Getenv("ALIBABA_CLOUD_ECS_METADATA") != "" { - ecsRamRoleProvider, err := NewECSRAMRoleCredentialsProviderBuilder().WithRoleName(os.Getenv("ALIBABA_CLOUD_ECS_METADATA")).Build() + ecsRamRoleProvider, err := NewECSRAMRoleCredentialsProviderBuilder().Build() + if err == nil { + providers = append(providers, ecsRamRoleProvider) + } + + // credentials uri + if os.Getenv("ALIBABA_CLOUD_CREDENTIALS_URI") != "" { + credentialsUriProvider, err := NewURLCredentialsProviderBuilderBuilder().Build() if err == nil { - providers = append(providers, ecsRamRoleProvider) + providers = append(providers, credentialsUriProvider) } } - // TODO: ALIBABA_CLOUD_CREDENTIALS_URI check - return &DefaultCredentialsProvider{ providerChain: providers, } diff --git a/credentials/providers/default_test.go b/credentials/providers/default_test.go index 989c128..5c971bc 100644 --- a/credentials/providers/default_test.go +++ b/credentials/providers/default_test.go @@ -11,7 +11,7 @@ import ( func TestDefaultCredentialsProvider(t *testing.T) { provider := NewDefaultCredentialsProvider() assert.NotNil(t, provider) - assert.Len(t, provider.providerChain, 3) + assert.Len(t, provider.providerChain, 4) _, ok := provider.providerChain[0].(*EnvironmentVariableCredentialsProvider) assert.True(t, ok) @@ -21,11 +21,15 @@ func TestDefaultCredentialsProvider(t *testing.T) { _, ok = provider.providerChain[2].(*ProfileCredentialsProvider) assert.True(t, ok) + _, ok = provider.providerChain[3].(*ECSRAMRoleCredentialsProvider) + assert.True(t, ok) + // Add oidc provider rollback := utils.Memory("ALIBABA_CLOUD_OIDC_TOKEN_FILE", "ALIBABA_CLOUD_OIDC_PROVIDER_ARN", "ALIBABA_CLOUD_ROLE_ARN", - "ALIBABA_CLOUD_ECS_METADATA") + "ALIBABA_CLOUD_ECS_METADATA", + "ALIBABA_CLOUD_CREDENTIALS_URI") defer rollback() os.Setenv("ALIBABA_CLOUD_OIDC_TOKEN_FILE", "/path/to/oidc.token") @@ -34,7 +38,7 @@ func TestDefaultCredentialsProvider(t *testing.T) { provider = NewDefaultCredentialsProvider() assert.NotNil(t, provider) - assert.Len(t, provider.providerChain, 4) + assert.Len(t, provider.providerChain, 5) _, ok = provider.providerChain[0].(*EnvironmentVariableCredentialsProvider) assert.True(t, ok) @@ -47,7 +51,10 @@ func TestDefaultCredentialsProvider(t *testing.T) { _, ok = provider.providerChain[3].(*ProfileCredentialsProvider) assert.True(t, ok) - // Add ecs ram role + _, ok = provider.providerChain[4].(*ECSRAMRoleCredentialsProvider) + assert.True(t, ok) + + // Add ecs ram role name os.Setenv("ALIBABA_CLOUD_ECS_METADATA", "rolename") provider = NewDefaultCredentialsProvider() assert.NotNil(t, provider) @@ -66,12 +73,36 @@ func TestDefaultCredentialsProvider(t *testing.T) { _, ok = provider.providerChain[4].(*ECSRAMRoleCredentialsProvider) assert.True(t, ok) + + // Add ecs ram role + os.Setenv("ALIBABA_CLOUD_CREDENTIALS_URI", "http://") + provider = NewDefaultCredentialsProvider() + assert.NotNil(t, provider) + assert.Len(t, provider.providerChain, 6) + _, ok = provider.providerChain[0].(*EnvironmentVariableCredentialsProvider) + assert.True(t, ok) + + _, ok = provider.providerChain[1].(*OIDCCredentialsProvider) + assert.True(t, ok) + + _, ok = provider.providerChain[2].(*CLIProfileCredentialsProvider) + assert.True(t, ok) + + _, ok = provider.providerChain[3].(*ProfileCredentialsProvider) + assert.True(t, ok) + + _, ok = provider.providerChain[4].(*ECSRAMRoleCredentialsProvider) + assert.True(t, ok) + + _, ok = provider.providerChain[5].(*URLCredentialsProvider) + assert.True(t, ok) } func TestDefaultCredentialsProvider_GetCredentials(t *testing.T) { rollback := utils.Memory("ALIBABA_CLOUD_ACCESS_KEY_ID", "ALIBABA_CLOUD_ACCESS_KEY_SECRET", - "ALIBABA_CLOUD_SECURITY_TOKEN") + "ALIBABA_CLOUD_SECURITY_TOKEN", + "ALIBABA_CLOUD_ECS_METADATA_DISABLED") defer func() { getHomePath = utils.GetHomePath @@ -83,6 +114,7 @@ func TestDefaultCredentialsProvider_GetCredentials(t *testing.T) { return "" } + os.Setenv("ALIBABA_CLOUD_ECS_METADATA_DISABLED", "true") provider := NewDefaultCredentialsProvider() assert.Len(t, provider.providerChain, 3) _, err := provider.GetCredentials() diff --git a/credentials/providers/ecs_ram_role.go b/credentials/providers/ecs_ram_role.go index 89be5eb..9a917b2 100644 --- a/credentials/providers/ecs_ram_role.go +++ b/credentials/providers/ecs_ram_role.go @@ -2,6 +2,7 @@ package providers import ( "encoding/json" + "errors" "fmt" "os" "strconv" @@ -17,6 +18,8 @@ type ECSRAMRoleCredentialsProvider struct { // for sts session *sessionCredentials expirationTimestamp int64 + // for http options + httpOptions *HttpOptions } type ECSRAMRoleCredentialsProviderBuilder struct { @@ -39,10 +42,20 @@ func (builder *ECSRAMRoleCredentialsProviderBuilder) WithDisableIMDSv1(disableIM return builder } +func (builder *ECSRAMRoleCredentialsProviderBuilder) WithHttpOptions(httpOptions *HttpOptions) *ECSRAMRoleCredentialsProviderBuilder { + builder.provider.httpOptions = httpOptions + return builder +} + const defaultMetadataTokenDuration = 21600 // 6 hours func (builder *ECSRAMRoleCredentialsProviderBuilder) Build() (provider *ECSRAMRoleCredentialsProvider, err error) { + if strings.ToLower(os.Getenv("ALIBABA_CLOUD_ECS_METADATA_DISABLED")) == "true" { + err = errors.New("IMDS credentials is disabled") + return + } + // 设置 roleName 默认值 if builder.provider.roleName == "" { builder.provider.roleName = os.Getenv("ALIBABA_CLOUD_ECS_METADATA") @@ -75,14 +88,27 @@ func (provider *ECSRAMRoleCredentialsProvider) needUpdateCredential() bool { func (provider *ECSRAMRoleCredentialsProvider) getRoleName() (roleName string, err error) { req := &httputil.Request{ - Method: "GET", - Protocol: "http", - Host: "100.100.100.200", - Path: "/latest/meta-data/ram/security-credentials/", - ConnectTimeout: 5 * time.Second, - ReadTimeout: 5 * time.Second, - Headers: map[string]string{}, + Method: "GET", + Protocol: "http", + Host: "100.100.100.200", + Path: "/latest/meta-data/ram/security-credentials/", + Headers: map[string]string{}, + } + + connectTimeout := 1 * time.Second + readTimeout := 1 * time.Second + + if provider.httpOptions != nil && provider.httpOptions.ConnectTimeout > 0 { + connectTimeout = time.Duration(provider.httpOptions.ConnectTimeout) * time.Millisecond + } + if provider.httpOptions != nil && provider.httpOptions.ReadTimeout > 0 { + readTimeout = time.Duration(provider.httpOptions.ReadTimeout) * time.Millisecond + } + if provider.httpOptions != nil && provider.httpOptions.Proxy != "" { + req.Proxy = provider.httpOptions.Proxy } + req.ConnectTimeout = connectTimeout + req.ReadTimeout = readTimeout metadataToken, err := provider.getMetadataToken() if err != nil { @@ -117,14 +143,27 @@ func (provider *ECSRAMRoleCredentialsProvider) getCredentials() (session *sessio } req := &httputil.Request{ - Method: "GET", - Protocol: "http", - Host: "100.100.100.200", - Path: "/latest/meta-data/ram/security-credentials/" + roleName, - ConnectTimeout: 5 * time.Second, - ReadTimeout: 5 * time.Second, - Headers: map[string]string{}, + Method: "GET", + Protocol: "http", + Host: "100.100.100.200", + Path: "/latest/meta-data/ram/security-credentials/" + roleName, + Headers: map[string]string{}, + } + + connectTimeout := 1 * time.Second + readTimeout := 1 * time.Second + + if provider.httpOptions != nil && provider.httpOptions.ConnectTimeout > 0 { + connectTimeout = time.Duration(provider.httpOptions.ConnectTimeout) * time.Millisecond + } + if provider.httpOptions != nil && provider.httpOptions.ReadTimeout > 0 { + readTimeout = time.Duration(provider.httpOptions.ReadTimeout) * time.Millisecond + } + if provider.httpOptions != nil && provider.httpOptions.Proxy != "" { + req.Proxy = provider.httpOptions.Proxy } + req.ConnectTimeout = connectTimeout + req.ReadTimeout = readTimeout metadataToken, err := provider.getMetadataToken() if err != nil { @@ -209,9 +248,23 @@ func (provider *ECSRAMRoleCredentialsProvider) getMetadataToken() (metadataToken Headers: map[string]string{ "X-aliyun-ecs-metadata-token-ttl-seconds": strconv.Itoa(defaultMetadataTokenDuration), }, - ConnectTimeout: 5 * time.Second, - ReadTimeout: 5 * time.Second, } + + connectTimeout := 1 * time.Second + readTimeout := 1 * time.Second + + if provider.httpOptions != nil && provider.httpOptions.ConnectTimeout > 0 { + connectTimeout = time.Duration(provider.httpOptions.ConnectTimeout) * time.Millisecond + } + if provider.httpOptions != nil && provider.httpOptions.ReadTimeout > 0 { + readTimeout = time.Duration(provider.httpOptions.ReadTimeout) * time.Millisecond + } + if provider.httpOptions != nil && provider.httpOptions.Proxy != "" { + req.Proxy = provider.httpOptions.Proxy + } + req.ConnectTimeout = connectTimeout + req.ReadTimeout = readTimeout + res, _err := httpDo(req) if _err != nil { if provider.disableIMDSv1 { diff --git a/credentials/providers/ecs_ram_role_test.go b/credentials/providers/ecs_ram_role_test.go index fae800c..064ebe8 100644 --- a/credentials/providers/ecs_ram_role_test.go +++ b/credentials/providers/ecs_ram_role_test.go @@ -7,17 +7,42 @@ import ( "time" httputil "github.com/aliyun/credentials-go/credentials/internal/http" + "github.com/aliyun/credentials-go/credentials/internal/utils" "github.com/stretchr/testify/assert" ) func TestNewECSRAMRoleCredentialsProvider(t *testing.T) { + rollback := utils.Memory("ALIBABA_CLOUD_ECS_METADATA_DISABLED", "ALIBABA_CLOUD_ECS_METADATA", "ALIBABA_CLOUD_IMDSV1_DISABLED") + defer func() { + rollback() + }() p, err := NewECSRAMRoleCredentialsProviderBuilder().Build() assert.Nil(t, err) assert.Equal(t, "", p.roleName) + os.Setenv("ALIBABA_CLOUD_ECS_METADATA", "rolename") + p, err = NewECSRAMRoleCredentialsProviderBuilder().Build() + assert.Nil(t, err) + assert.Equal(t, "rolename", p.roleName) + p, err = NewECSRAMRoleCredentialsProviderBuilder().WithRoleName("role").Build() assert.Nil(t, err) assert.Equal(t, "role", p.roleName) + assert.False(t, p.disableIMDSv1) + + os.Setenv("ALIBABA_CLOUD_IMDSV1_DISABLED", "True") + p, err = NewECSRAMRoleCredentialsProviderBuilder().Build() + assert.Nil(t, err) + assert.True(t, p.disableIMDSv1) + + os.Setenv("ALIBABA_CLOUD_IMDSV1_DISABLED", "1") + p, err = NewECSRAMRoleCredentialsProviderBuilder().WithDisableIMDSv1(true).Build() + assert.Nil(t, err) + assert.True(t, p.disableIMDSv1) + + os.Setenv("ALIBABA_CLOUD_ECS_METADATA_DISABLED", "True") + _, err = NewECSRAMRoleCredentialsProviderBuilder().Build() + assert.Equal(t, "IMDS credentials is disabled", err.Error()) assert.True(t, p.needUpdateCredential()) } @@ -367,6 +392,11 @@ func TestECSRAMRoleCredentialsProviderGetCredentials(t *testing.T) { } func TestECSRAMRoleCredentialsProvider_getMetadataToken(t *testing.T) { + rollback := utils.Memory("ALIBABA_CLOUD_IMDSV1_DISABLED") + defer func() { + rollback() + }() + originHttpDo := httpDo defer func() { httpDo = originHttpDo }() @@ -444,3 +474,27 @@ func TestECSRAMRoleCredentialsProvider_getMetadataToken(t *testing.T) { assert.NotNil(t, err) assert.Equal(t, "", metadataToken) } + +func TestNewECSRAMRoleCredentialsProviderWithHttpOptions(t *testing.T) { + p, err := NewECSRAMRoleCredentialsProviderBuilder(). + WithRoleName("test"). + WithHttpOptions(&HttpOptions{ + ConnectTimeout: 1000, + ReadTimeout: 1000, + Proxy: "localhost:3999", + }). + Build() + assert.Nil(t, err) + + _, err = p.getRoleName() + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "proxyconnect tcp:") + + _, err = p.getCredentials() + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "proxyconnect tcp:") + + _, err = p.GetCredentials() + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "proxyconnect tcp:") +} diff --git a/credentials/providers/oidc.go b/credentials/providers/oidc.go index c7a3a48..ae7194c 100644 --- a/credentials/providers/oidc.go +++ b/credentials/providers/oidc.go @@ -163,11 +163,20 @@ func (provider *OIDCCredentialsProvider) getCredentials() (session *sessionCrede Headers: map[string]string{}, } - if provider.httpOptions != nil { - req.ConnectTimeout = time.Duration(provider.httpOptions.ConnectTimeout) * time.Second - req.ReadTimeout = time.Duration(provider.httpOptions.ReadTimeout) * time.Second + connectTimeout := 5 * time.Second + readTimeout := 10 * time.Second + + if provider.httpOptions != nil && provider.httpOptions.ConnectTimeout > 0 { + connectTimeout = time.Duration(provider.httpOptions.ConnectTimeout) * time.Millisecond + } + if provider.httpOptions != nil && provider.httpOptions.ReadTimeout > 0 { + readTimeout = time.Duration(provider.httpOptions.ReadTimeout) * time.Millisecond + } + if provider.httpOptions != nil && provider.httpOptions.Proxy != "" { req.Proxy = provider.httpOptions.Proxy } + req.ConnectTimeout = connectTimeout + req.ReadTimeout = readTimeout queries := make(map[string]string) queries["Version"] = "2015-04-01" diff --git a/credentials/providers/oidc_test.go b/credentials/providers/oidc_test.go index 204039f..3d46660 100644 --- a/credentials/providers/oidc_test.go +++ b/credentials/providers/oidc_test.go @@ -24,12 +24,12 @@ func TestOIDCCredentialsProviderGetCredentialsWithError(t *testing.T) { WithPolicy("policy"). WithDurationSeconds(1000). WithHttpOptions(&HttpOptions{ - ConnectTimeout: 10, + ConnectTimeout: 10000, }). Build() assert.Nil(t, err) - assert.Equal(t, 10, p.httpOptions.ConnectTimeout) + assert.Equal(t, 10000, p.httpOptions.ConnectTimeout) _, err = p.GetCredentials() assert.NotNil(t, err) assert.Contains(t, err.Error(), "AuthenticationFail.NoPermission") @@ -337,3 +337,26 @@ func TestOIDCCredentialsProviderGetCredentials(t *testing.T) { assert.Equal(t, "oidc_role_arn", cc.ProviderName) assert.True(t, p.needUpdateCredential()) } + +func TestOIDCCredentialsProviderGetCredentialsWithHttpOptions(t *testing.T) { + wd, _ := os.Getwd() + p, err := NewOIDCCredentialsProviderBuilder(). + // read a normal token + WithOIDCTokenFilePath(path.Join(wd, "fixtures/mock_oidctoken")). + WithOIDCProviderARN("provider-arn"). + WithRoleArn("roleArn"). + WithRoleSessionName("rsn"). + WithPolicy("policy"). + WithDurationSeconds(1000). + WithHttpOptions(&HttpOptions{ + ConnectTimeout: 1000, + ReadTimeout: 1000, + Proxy: "localhost:3999", + }). + Build() + + assert.Nil(t, err) + _, err = p.GetCredentials() + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "proxyconnect tcp:") +} diff --git a/credentials/providers/ram_role_arn.go b/credentials/providers/ram_role_arn.go index da4ffb2..cd25302 100644 --- a/credentials/providers/ram_role_arn.go +++ b/credentials/providers/ram_role_arn.go @@ -39,9 +39,11 @@ type sessionCredentials struct { } type HttpOptions struct { - Proxy string + Proxy string + // Connection timeout, in milliseconds. ConnectTimeout int - ReadTimeout int + // Read timeout, in milliseconds. + ReadTimeout int } type RAMRoleARNCredentialsProvider struct { @@ -164,8 +166,8 @@ func (builder *RAMRoleARNCredentialsProviderBuilder) Build() (provider *RAMRoleA } } else { err = errors.New("must specify a previous credentials provider to assume role") + return } - return } if builder.provider.roleArn == "" { @@ -278,11 +280,20 @@ func (provider *RAMRoleARNCredentialsProvider) getCredentials(cc *Credentials) ( req.Headers["Content-Type"] = "application/x-www-form-urlencoded" req.Headers["x-acs-credentials-provider"] = cc.ProviderName - if provider.httpOptions != nil { - req.ConnectTimeout = time.Duration(provider.httpOptions.ConnectTimeout) * time.Second - req.ReadTimeout = time.Duration(provider.httpOptions.ReadTimeout) * time.Second + connectTimeout := 5 * time.Second + readTimeout := 10 * time.Second + + if provider.httpOptions != nil && provider.httpOptions.ConnectTimeout > 0 { + connectTimeout = time.Duration(provider.httpOptions.ConnectTimeout) * time.Millisecond + } + if provider.httpOptions != nil && provider.httpOptions.ReadTimeout > 0 { + readTimeout = time.Duration(provider.httpOptions.ReadTimeout) * time.Millisecond + } + if provider.httpOptions != nil && provider.httpOptions.Proxy != "" { req.Proxy = provider.httpOptions.Proxy } + req.ConnectTimeout = connectTimeout + req.ReadTimeout = readTimeout res, err := httpDo(req) if err != nil { diff --git a/credentials/providers/ram_role_arn_test.go b/credentials/providers/ram_role_arn_test.go index 77886ca..57f0ec2 100644 --- a/credentials/providers/ram_role_arn_test.go +++ b/credentials/providers/ram_role_arn_test.go @@ -2,6 +2,7 @@ package providers import ( "errors" + "fmt" "os" "strings" "testing" @@ -77,7 +78,7 @@ func TestNewRAMRoleARNCredentialsProvider(t *testing.T) { // sts endpoint with sts region assert.Equal(t, "sts-vpc.cn-hangzhou.aliyuncs.com", p.stsEndpoint) - // default sts endpoint + // case 7: check default sts endpoint os.Setenv("ALIBABA_CLOUD_VPC_ENDPOINT_ENABLED", "1") p, err = NewRAMRoleARNCredentialsProviderBuilder(). WithCredentialsProvider(akProvider). @@ -96,7 +97,7 @@ func TestNewRAMRoleARNCredentialsProvider(t *testing.T) { assert.Equal(t, 1000, p.durationSeconds) assert.Equal(t, "sts.aliyuncs.com", p.stsEndpoint) - // sts endpoint with env + // case 8: check sts endpoint with env os.Setenv("ALIBABA_CLOUD_STS_REGION", "cn-hangzhou") os.Setenv("ALIBABA_CLOUD_VPC_ENDPOINT_ENABLED", "True") p, err = NewRAMRoleARNCredentialsProviderBuilder(). @@ -110,7 +111,7 @@ func TestNewRAMRoleARNCredentialsProvider(t *testing.T) { assert.Nil(t, err) assert.Equal(t, "sts-vpc.cn-hangzhou.aliyuncs.com", p.stsEndpoint) - // sts endpoint with sts endpoint + // case 9: check sts endpoint with sts endpoint p, err = NewRAMRoleARNCredentialsProviderBuilder(). WithCredentialsProvider(akProvider). WithRoleArn("roleArn"). @@ -128,6 +129,45 @@ func TestNewRAMRoleARNCredentialsProvider(t *testing.T) { assert.Equal(t, "", p.stsRegionId) assert.Equal(t, 1000, p.durationSeconds) assert.Equal(t, "sts.cn-shanghai.aliyuncs.com", p.stsEndpoint) + + // case 10: check ak&sk + p, err = NewRAMRoleARNCredentialsProviderBuilder(). + WithAccessKeyId("ak"). + WithAccessKeySecret("sk"). + WithRoleArn("roleArn"). + WithStsEndpoint("sts.cn-shanghai.aliyuncs.com"). + WithPolicy("policy"). + WithExternalId("externalId"). + WithRoleSessionName("rsn"). + WithDurationSeconds(1000). + Build() + assert.Nil(t, err) + fmt.Println(p.credentialsProvider) + cre, err := p.credentialsProvider.GetCredentials() + assert.Nil(t, err) + assert.Equal(t, "ak", cre.AccessKeyId) + assert.Equal(t, "sk", cre.AccessKeySecret) + assert.Equal(t, "static_ak", cre.ProviderName) + + // case 11: check ak&sk&token + p, err = NewRAMRoleARNCredentialsProviderBuilder(). + WithAccessKeyId("ak"). + WithAccessKeySecret("sk"). + WithSecurityToken("token"). + WithRoleArn("roleArn"). + WithStsEndpoint("sts.cn-shanghai.aliyuncs.com"). + WithPolicy("policy"). + WithExternalId("externalId"). + WithRoleSessionName("rsn"). + WithDurationSeconds(1000). + Build() + assert.Nil(t, err) + cre, err = p.credentialsProvider.GetCredentials() + assert.Nil(t, err) + assert.Equal(t, "ak", cre.AccessKeyId) + assert.Equal(t, "sk", cre.AccessKeySecret) + assert.Equal(t, "token", cre.SecurityToken) + assert.Equal(t, "static_sts", cre.ProviderName) } func TestRAMRoleARNCredentialsProvider_getCredentials(t *testing.T) { @@ -390,8 +430,8 @@ func TestRAMRoleARNCredentialsProviderWithHttpOptions(t *testing.T) { WithRoleSessionName("rsn"). WithDurationSeconds(1000). WithHttpOptions(&HttpOptions{ - ConnectTimeout: 1, - ReadTimeout: 1, + ConnectTimeout: 1000, + ReadTimeout: 1000, Proxy: "localhost:3999", }). Build() diff --git a/credentials/providers/uri.go b/credentials/providers/uri.go new file mode 100644 index 0000000..6572805 --- /dev/null +++ b/credentials/providers/uri.go @@ -0,0 +1,152 @@ +package providers + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "os" + "time" + + httputil "github.com/aliyun/credentials-go/credentials/internal/http" +) + +type URLCredentialsProvider struct { + url string + // for sts + sessionCredentials *sessionCredentials + // for http options + httpOptions *HttpOptions + // inner + expirationTimestamp int64 +} + +type URLCredentialsProviderBuilder struct { + provider *URLCredentialsProvider +} + +func NewURLCredentialsProviderBuilderBuilder() *URLCredentialsProviderBuilder { + return &URLCredentialsProviderBuilder{ + provider: &URLCredentialsProvider{}, + } +} + +func (builder *URLCredentialsProviderBuilder) WithUrl(url string) *URLCredentialsProviderBuilder { + builder.provider.url = url + return builder +} + +func (builder *URLCredentialsProviderBuilder) WithHttpOptions(httpOptions *HttpOptions) *URLCredentialsProviderBuilder { + builder.provider.httpOptions = httpOptions + return builder +} + +func (builder *URLCredentialsProviderBuilder) Build() (provider *URLCredentialsProvider, err error) { + + if builder.provider.url == "" { + builder.provider.url = os.Getenv("ALIBABA_CLOUD_CREDENTIALS_URI") + } + + if builder.provider.url == "" { + err = errors.New("the url is empty") + return + } + + provider = builder.provider + return +} + +type urlResponse struct { + AccessKeyId *string `json:"AccessKeyId"` + AccessKeySecret *string `json:"AccessKeySecret"` + SecurityToken *string `json:"SecurityToken"` + Expiration *string `json:"Expiration"` +} + +func (provider *URLCredentialsProvider) getCredentials() (session *sessionCredentials, err error) { + req := &httputil.Request{ + Method: "GET", + URL: provider.url, + } + + connectTimeout := 5 * time.Second + readTimeout := 10 * time.Second + + if provider.httpOptions != nil && provider.httpOptions.ConnectTimeout > 0 { + connectTimeout = time.Duration(provider.httpOptions.ConnectTimeout) * time.Millisecond + } + if provider.httpOptions != nil && provider.httpOptions.ReadTimeout > 0 { + readTimeout = time.Duration(provider.httpOptions.ReadTimeout) * time.Millisecond + } + if provider.httpOptions != nil && provider.httpOptions.Proxy != "" { + req.Proxy = provider.httpOptions.Proxy + } + req.ConnectTimeout = connectTimeout + req.ReadTimeout = readTimeout + + res, err := httpDo(req) + if err != nil { + return + } + + if res.StatusCode != http.StatusOK { + err = fmt.Errorf("get credentials from %s failed: %s", req.BuildRequestURL(), string(res.Body)) + return + } + + var resp urlResponse + err = json.Unmarshal(res.Body, &resp) + if err != nil { + err = fmt.Errorf("get credentials from %s failed with error, json unmarshal fail: %s", req.BuildRequestURL(), err.Error()) + return + } + + if resp.AccessKeyId == nil || resp.AccessKeySecret == nil || resp.SecurityToken == nil || resp.Expiration == nil { + err = fmt.Errorf("refresh credentials from %s failed: %s", req.BuildRequestURL(), string(res.Body)) + return + } + + session = &sessionCredentials{ + AccessKeyId: *resp.AccessKeyId, + AccessKeySecret: *resp.AccessKeySecret, + SecurityToken: *resp.SecurityToken, + Expiration: *resp.Expiration, + } + return +} + +func (provider *URLCredentialsProvider) needUpdateCredential() (result bool) { + if provider.expirationTimestamp == 0 { + return true + } + + return provider.expirationTimestamp-time.Now().Unix() <= 180 +} + +func (provider *URLCredentialsProvider) GetCredentials() (cc *Credentials, err error) { + if provider.sessionCredentials == nil || provider.needUpdateCredential() { + sessionCredentials, err1 := provider.getCredentials() + if err1 != nil { + return nil, err1 + } + + provider.sessionCredentials = sessionCredentials + expirationTime, err2 := time.Parse("2006-01-02T15:04:05Z", sessionCredentials.Expiration) + if err2 != nil { + return nil, err2 + } + provider.expirationTimestamp = expirationTime.Unix() + } + + cc = &Credentials{ + AccessKeyId: provider.sessionCredentials.AccessKeyId, + AccessKeySecret: provider.sessionCredentials.AccessKeySecret, + SecurityToken: provider.sessionCredentials.SecurityToken, + ProviderName: provider.GetProviderName(), + } + return +} + +func (provider *URLCredentialsProvider) GetProviderName() string { + return "credential_uri" +} diff --git a/credentials/providers/uri_test.go b/credentials/providers/uri_test.go new file mode 100644 index 0000000..d891d40 --- /dev/null +++ b/credentials/providers/uri_test.go @@ -0,0 +1,199 @@ +package providers + +import ( + "errors" + "os" + "strings" + "testing" + "time" + + httputil "github.com/aliyun/credentials-go/credentials/internal/http" + "github.com/aliyun/credentials-go/credentials/internal/utils" + "github.com/stretchr/testify/assert" +) + +func TestNewURLCredentialsProvider(t *testing.T) { + rollback := utils.Memory("ALIBABA_CLOUD_CREDENTIALS_URI") + defer func() { + rollback() + }() + // case 1: no credentials provider + _, err := NewURLCredentialsProviderBuilderBuilder(). + Build() + assert.EqualError(t, err, "the url is empty") + + // case 2: no role arn + os.Setenv("ALIBABA_CLOUD_CREDENTIALS_URI", "http://localhost:8080") + p, err := NewURLCredentialsProviderBuilderBuilder(). + Build() + assert.Nil(t, err) + assert.True(t, strings.HasPrefix(p.url, "http://localhost:8080")) + + // case 3: check default role session name + p, err = NewURLCredentialsProviderBuilderBuilder(). + WithUrl("http://localhost:9090"). + Build() + assert.Nil(t, err) + assert.True(t, strings.HasPrefix(p.url, "http://localhost:9090")) +} + +func TestURLCredentialsProvider_getCredentials(t *testing.T) { + originHttpDo := httpDo + defer func() { httpDo = originHttpDo }() + p, err := NewURLCredentialsProviderBuilderBuilder(). + WithUrl("http://localhost:8080"). + Build() + assert.Nil(t, err) + + // case 1: server error + httpDo = func(req *httputil.Request) (res *httputil.Response, err error) { + err = errors.New("mock server error") + return + } + _, err = p.getCredentials() + assert.NotNil(t, err) + assert.Equal(t, "mock server error", err.Error()) + + // case 2: 4xx error + httpDo = func(req *httputil.Request) (res *httputil.Response, err error) { + res = &httputil.Response{ + StatusCode: 400, + Body: []byte("4xx error"), + } + return + } + + _, err = p.getCredentials() + assert.NotNil(t, err) + assert.Equal(t, "get credentials from GET http://localhost:8080 failed: 4xx error", err.Error()) + + // case 3: invalid json + httpDo = func(req *httputil.Request) (res *httputil.Response, err error) { + res = &httputil.Response{ + StatusCode: 200, + Body: []byte("invalid json"), + } + return + } + _, err = p.getCredentials() + assert.NotNil(t, err) + assert.Equal(t, "get credentials from GET http://localhost:8080 failed with error, json unmarshal fail: invalid character 'i' looking for beginning of value", err.Error()) + + // case 4: empty response json + httpDo = func(req *httputil.Request) (res *httputil.Response, err error) { + res = &httputil.Response{ + StatusCode: 200, + Body: []byte("null"), + } + return + } + _, err = p.getCredentials() + assert.NotNil(t, err) + assert.Equal(t, "refresh credentials from GET http://localhost:8080 failed: null", err.Error()) + + // case 5: empty session ak response json + httpDo = func(req *httputil.Request) (res *httputil.Response, err error) { + res = &httputil.Response{ + StatusCode: 200, + Body: []byte(`{}`), + } + return + } + _, err = p.getCredentials() + assert.NotNil(t, err) + assert.Equal(t, "refresh credentials from GET http://localhost:8080 failed: {}", err.Error()) + + // case 6: mock ok value + httpDo = func(req *httputil.Request) (res *httputil.Response, err error) { + res = &httputil.Response{ + StatusCode: 200, + Body: []byte(`{"AccessKeyId":"saki","AccessKeySecret":"saks","Expiration":"2021-10-20T04:27:09Z","SecurityToken":"token"}`), + } + return + } + creds, err := p.getCredentials() + assert.Nil(t, err) + assert.Equal(t, "saki", creds.AccessKeyId) + assert.Equal(t, "saks", creds.AccessKeySecret) + assert.Equal(t, "token", creds.SecurityToken) + assert.Equal(t, "2021-10-20T04:27:09Z", creds.Expiration) + + // needUpdateCredential + assert.True(t, p.needUpdateCredential()) + p.expirationTimestamp = time.Now().Unix() + assert.True(t, p.needUpdateCredential()) + + p.expirationTimestamp = time.Now().Unix() + 300 + assert.False(t, p.needUpdateCredential()) +} + +func TestURLCredentialsProvider_GetCredentials(t *testing.T) { + originHttpDo := httpDo + defer func() { httpDo = originHttpDo }() + + // case 0: get previous credentials failed + p, err := NewURLCredentialsProviderBuilderBuilder(). + WithUrl("http://localhost:8080"). + Build() + assert.Nil(t, err) + + // case 1: get credentials failed + httpDo = func(req *httputil.Request) (res *httputil.Response, err error) { + err = errors.New("mock server error") + return + } + _, err = p.GetCredentials() + assert.NotNil(t, err) + assert.Equal(t, "mock server error", err.Error()) + + // case 2: get invalid expiration + httpDo = func(req *httputil.Request) (res *httputil.Response, err error) { + res = &httputil.Response{ + StatusCode: 200, + Body: []byte(`{"AccessKeyId":"akid","AccessKeySecret":"aksecret","Expiration":"invalidexpiration","SecurityToken":"ststoken"}`), + } + return + } + _, err = p.GetCredentials() + assert.NotNil(t, err) + assert.Equal(t, "parsing time \"invalidexpiration\" as \"2006-01-02T15:04:05Z\": cannot parse \"invalidexpiration\" as \"2006\"", err.Error()) + + // case 3: happy result + httpDo = func(req *httputil.Request) (res *httputil.Response, err error) { + res = &httputil.Response{ + StatusCode: 200, + Body: []byte(`{"AccessKeyId":"akid","AccessKeySecret":"aksecret","Expiration":"2021-10-20T04:27:09Z","SecurityToken":"ststoken"}`), + } + return + } + cc, err := p.GetCredentials() + assert.Nil(t, err) + assert.Equal(t, "akid", cc.AccessKeyId) + assert.Equal(t, "aksecret", cc.AccessKeySecret) + assert.Equal(t, "ststoken", cc.SecurityToken) + assert.Equal(t, "credential_uri", cc.ProviderName) + assert.True(t, p.needUpdateCredential()) + // get credentials again + cc, err = p.GetCredentials() + assert.Nil(t, err) + assert.Equal(t, "akid", cc.AccessKeyId) + assert.Equal(t, "aksecret", cc.AccessKeySecret) + assert.Equal(t, "ststoken", cc.SecurityToken) + assert.Equal(t, "credential_uri", cc.ProviderName) + assert.True(t, p.needUpdateCredential()) +} + +func TestURLCredentialsProviderWithHttpOptions(t *testing.T) { + p, err := NewURLCredentialsProviderBuilderBuilder(). + WithUrl("http://localhost:8080"). + WithHttpOptions(&HttpOptions{ + ConnectTimeout: 1000, + ReadTimeout: 1000, + Proxy: "localhost:3999", + }). + Build() + assert.Nil(t, err) + _, err = p.GetCredentials() + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "proxyconnect tcp:") +}