package imds import ( "bytes" "context" "errors" "fmt" "io" "net/http" "os" "strings" "testing" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws/retry" "github.com/aws/aws-sdk-go-v2/feature/ec2/imds/internal/config" "github.com/aws/aws-sdk-go-v2/internal/awstesting" smithyhttp "github.com/aws/smithy-go/transport/http" ) func TestClientEndpoint(t *testing.T) { cases := map[string]struct { Endpoint string EndpointMode EndpointModeState EnvVar map[string]string Expect string WantErr bool }{ "default": { Expect: defaultIPv4Endpoint, }, "options endpoint mode IPv4": { EndpointMode: EndpointModeStateIPv4, Expect: defaultIPv4Endpoint, }, "options endpoint mode IPv6": { EndpointMode: EndpointModeStateIPv6, Expect: defaultIPv6Endpoint, }, "options endpoint mode IPv6 AND options endpoint": { Endpoint: "http://endpoint.localhost", EndpointMode: EndpointModeStateIPv6, Expect: "http://endpoint.localhost", }, "options endpoint mode IPv4 AND options endpoint": { Endpoint: "http://endpoint.localhost", EndpointMode: EndpointModeStateIPv4, Expect: "http://endpoint.localhost", }, "options endpoint": { Endpoint: "http://endpoint.localhost", Expect: "http://endpoint.localhost", }, "options endpoint AND env endpoint": { Endpoint: "http://endpoint.localhost", EnvVar: map[string]string{ endpointEnvVar: "http://[::1]", }, Expect: "http://endpoint.localhost", }, "env endpoint": { EnvVar: map[string]string{ endpointEnvVar: "http://[::1]", }, Expect: "http://[::1]", }, "env endpoint missing scheme": { EnvVar: map[string]string{ endpointEnvVar: "[::1]", }, WantErr: true, }, "options endpoint missing scheme": { Endpoint: "[::1]", WantErr: true, }, } for name, c := range cases { t.Run(name, func(t *testing.T) { envs := awstesting.StashEnv() defer awstesting.PopEnv(envs) if v := c.EnvVar; len(v) != 0 { for k, v := range c.EnvVar { if err := os.Setenv(k, v); err != nil { t.Errorf("expect no error, got %v", err) } } } client := New(Options{ disableAPIToken: true, Endpoint: c.Endpoint, EndpointMode: c.EndpointMode, HTTPClient: smithyhttp.ClientDoFunc(func(r *http.Request) (*http.Response, error) { if e, a := c.Expect+getMetadataPath, r.URL.String(); e != a { return nil, fmt.Errorf("expect %v endpoint, got %v", e, a) } return newMockResponse(), nil }), }) _, err := client.GetMetadata(context.Background(), nil) if (err != nil) != c.WantErr { t.Fatalf("WantErr=%v, got err=%v", c.WantErr, err) } }) } } func TestClientEnableState(t *testing.T) { cases := map[string]struct { EnvironmentVar string ClientEnableState ClientEnableState ExpectErr string }{ "default": {}, "disabled environment var": { EnvironmentVar: "true", ExpectErr: "access disabled", }, "unknown environment value": { EnvironmentVar: "blah", }, "disabled option": { ClientEnableState: ClientDisabled, ExpectErr: "access disabled", }, "enabled option": { EnvironmentVar: "true", ClientEnableState: ClientEnabled, }, } for name, c := range cases { t.Run(name, func(t *testing.T) { envs := awstesting.StashEnv() defer awstesting.PopEnv(envs) if v := c.EnvironmentVar; len(v) != 0 { os.Setenv(disableClientEnvVar, v) } client := New(Options{ disableAPIToken: true, ClientEnableState: c.ClientEnableState, HTTPClient: smithyhttp.ClientDoFunc(func(r *http.Request) (*http.Response, error) { return newMockResponse(), nil }), }) _, err := client.GetMetadata(context.Background(), nil) if len(c.ExpectErr) != 0 { if err == nil { t.Fatalf("expect error, got none") } if e, a := c.ExpectErr, err.Error(); !strings.Contains(a, e) { t.Fatalf("expect error to contain %v, got %v", e, a) } return } if err != nil { t.Fatalf("expect no error, got %v", err) } }) } } type WithEndpointModeSource EndpointModeState func (w WithEndpointModeSource) GetEC2IMDSEndpointMode() (config.EndpointModeState, bool, error) { return config.EndpointModeState(w), true, nil } type WithEndpoint string func (w WithEndpoint) GetEC2IMDSEndpoint() (string, bool, error) { return string(w), true, nil } func TestNewFromConfig(t *testing.T) { cases := map[string]struct { Sources []any Expect string }{ "default": { Expect: defaultIPv4Endpoint, }, "non-implementing sources": { Sources: []any{ struct{}{}, }, Expect: defaultIPv4Endpoint, }, "endpoint mode IPv6": { Sources: []any{ WithEndpointModeSource(EndpointModeStateIPv6), }, Expect: defaultIPv6Endpoint, }, "endpoint mode IPv4": { Sources: []any{ WithEndpointModeSource(EndpointModeStateIPv4), }, Expect: defaultIPv4Endpoint, }, "endpoint mode unknown": { Sources: []any{ WithEndpointModeSource(func() (v EndpointModeState) { v.SetFromString("foobar") return v }()), }, Expect: defaultIPv4Endpoint, }, "endpoint": { Sources: []any{ WithEndpoint("http://endpoint.localhost"), }, Expect: "http://endpoint.localhost", }, "endpoint mode && endpoint": { Sources: []any{ WithEndpointModeSource(EndpointModeStateIPv6), WithEndpoint("http://endpoint.localhost"), }, Expect: "http://endpoint.localhost", }, } for name, tt := range cases { t.Run(name, func(t *testing.T) { envs := awstesting.StashEnv() defer awstesting.PopEnv(envs) client := NewFromConfig(aws.Config{ ConfigSources: tt.Sources, }, func(options *Options) { options.disableAPIToken = true options.HTTPClient = smithyhttp.ClientDoFunc(func(r *http.Request) (*http.Response, error) { if e, a := tt.Expect+getMetadataPath, r.URL.String(); e != a { return nil, fmt.Errorf("expect %v endpoint, got %v", e, a) } return newMockResponse(), nil }) }) _, err := client.GetMetadata(context.Background(), nil) if err != nil { t.Fatal(err) } }) } } func TestClientRetryerBackoff(t *testing.T) { cases := map[string]struct { Retryer aws.Retryer DisableDefaultMaxBackoff bool ExpectErr string ExpectRetryDelay time.Duration }{ "default 1-second backoff": { ExpectRetryDelay: 1 * time.Second, }, "disable default backoff and use standard retryer backoff": { DisableDefaultMaxBackoff: true, ExpectRetryDelay: 20 * time.Second, }, "disable default backoff and use customize retryer": { Retryer: retry.NewStandard(func(o *retry.StandardOptions) { o.Backoff = &mockDelayer{ delay: 1000 * time.Second, } }), DisableDefaultMaxBackoff: true, ExpectRetryDelay: 1000 * time.Second, }, "disable default backoff and use customize retryer returning error": { Retryer: retry.NewStandard(func(o *retry.StandardOptions) { o.Backoff = &mockDelayer{ delay: 1 * time.Second, err: "retryBackoffError", } }), DisableDefaultMaxBackoff: true, ExpectErr: "retryBackoffError", }, } for name, c := range cases { t.Run(name, func(t *testing.T) { client := New(Options{ Retryer: c.Retryer, DisableDefaultMaxBackoff: c.DisableDefaultMaxBackoff, }) delay, err := client.options.Retryer.RetryDelay(10, nil) if err != nil { if c.ExpectErr == "" { t.Fatalf("expect no error, got %v", err) } else if e, a := c.ExpectErr, err.Error(); !strings.Contains(a, e) { t.Fatalf("expect %s to be in %v", e, a) } } else if c.ExpectErr != "" { t.Fatalf("expect error returned, but got none") } if c.ExpectErr != "" { return } if e, a := c.ExpectRetryDelay/time.Second, delay/time.Second; e != a { t.Fatalf("expect retry delay to be %d seconds, got %d", e, a) } }) } } type mockDelayer struct { delay time.Duration err string } // BackoffDelay implements retry.BackoffDelayer.BackoffDelay() func (d *mockDelayer) BackoffDelay(attempt int, err error) (time.Duration, error) { if d.err != "" { return 0, errors.New(d.err) } return d.delay, nil } func newMockResponse() *http.Response { return &http.Response{ StatusCode: 200, Header: http.Header{}, Body: io.NopCloser(bytes.NewReader([]byte{})), } }