api_client.go

  1package imds
  2
  3import (
  4	"context"
  5	"fmt"
  6	"net"
  7	"net/http"
  8	"os"
  9	"strings"
 10	"time"
 11
 12	"github.com/aws/aws-sdk-go-v2/aws"
 13	"github.com/aws/aws-sdk-go-v2/aws/retry"
 14	awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
 15	internalconfig "github.com/aws/aws-sdk-go-v2/feature/ec2/imds/internal/config"
 16	"github.com/aws/smithy-go"
 17	"github.com/aws/smithy-go/logging"
 18	"github.com/aws/smithy-go/middleware"
 19	smithyhttp "github.com/aws/smithy-go/transport/http"
 20)
 21
 22// ServiceID provides the unique name of this API client
 23const ServiceID = "ec2imds"
 24
 25// Client provides the API client for interacting with the Amazon EC2 Instance
 26// Metadata Service API.
 27type Client struct {
 28	options Options
 29}
 30
 31// ClientEnableState provides an enumeration if the client is enabled,
 32// disabled, or default behavior.
 33type ClientEnableState = internalconfig.ClientEnableState
 34
 35// Enumeration values for ClientEnableState
 36const (
 37	ClientDefaultEnableState ClientEnableState = internalconfig.ClientDefaultEnableState // default behavior
 38	ClientDisabled           ClientEnableState = internalconfig.ClientDisabled           // client disabled
 39	ClientEnabled            ClientEnableState = internalconfig.ClientEnabled            // client enabled
 40)
 41
 42// EndpointModeState is an enum configuration variable describing the client endpoint mode.
 43// Not configurable directly, but used when using the NewFromConfig.
 44type EndpointModeState = internalconfig.EndpointModeState
 45
 46// Enumeration values for EndpointModeState
 47const (
 48	EndpointModeStateUnset EndpointModeState = internalconfig.EndpointModeStateUnset
 49	EndpointModeStateIPv4  EndpointModeState = internalconfig.EndpointModeStateIPv4
 50	EndpointModeStateIPv6  EndpointModeState = internalconfig.EndpointModeStateIPv6
 51)
 52
 53const (
 54	disableClientEnvVar = "AWS_EC2_METADATA_DISABLED"
 55
 56	// Client endpoint options
 57	endpointEnvVar = "AWS_EC2_METADATA_SERVICE_ENDPOINT"
 58
 59	defaultIPv4Endpoint = "http://169.254.169.254"
 60	defaultIPv6Endpoint = "http://[fd00:ec2::254]"
 61)
 62
 63// New returns an initialized Client based on the functional options. Provide
 64// additional functional options to further configure the behavior of the client,
 65// such as changing the client's endpoint or adding custom middleware behavior.
 66func New(options Options, optFns ...func(*Options)) *Client {
 67	options = options.Copy()
 68
 69	for _, fn := range optFns {
 70		fn(&options)
 71	}
 72
 73	options.HTTPClient = resolveHTTPClient(options.HTTPClient)
 74
 75	if options.Retryer == nil {
 76		options.Retryer = retry.NewStandard()
 77	}
 78	options.Retryer = retry.AddWithMaxBackoffDelay(options.Retryer, 1*time.Second)
 79
 80	if options.ClientEnableState == ClientDefaultEnableState {
 81		if v := os.Getenv(disableClientEnvVar); strings.EqualFold(v, "true") {
 82			options.ClientEnableState = ClientDisabled
 83		}
 84	}
 85
 86	if len(options.Endpoint) == 0 {
 87		if v := os.Getenv(endpointEnvVar); len(v) != 0 {
 88			options.Endpoint = v
 89		}
 90	}
 91
 92	client := &Client{
 93		options: options,
 94	}
 95
 96	if client.options.tokenProvider == nil && !client.options.disableAPIToken {
 97		client.options.tokenProvider = newTokenProvider(client, defaultTokenTTL)
 98	}
 99
100	return client
101}
102
103// NewFromConfig returns an initialized Client based the AWS SDK config, and
104// functional options. Provide additional functional options to further
105// configure the behavior of the client, such as changing the client's endpoint
106// or adding custom middleware behavior.
107func NewFromConfig(cfg aws.Config, optFns ...func(*Options)) *Client {
108	opts := Options{
109		APIOptions:    append([]func(*middleware.Stack) error{}, cfg.APIOptions...),
110		HTTPClient:    cfg.HTTPClient,
111		ClientLogMode: cfg.ClientLogMode,
112		Logger:        cfg.Logger,
113	}
114
115	if cfg.Retryer != nil {
116		opts.Retryer = cfg.Retryer()
117	}
118
119	resolveClientEnableState(cfg, &opts)
120	resolveEndpointConfig(cfg, &opts)
121	resolveEndpointModeConfig(cfg, &opts)
122	resolveEnableFallback(cfg, &opts)
123
124	return New(opts, optFns...)
125}
126
127// Options provides the fields for configuring the API client's behavior.
128type Options struct {
129	// Set of options to modify how an operation is invoked. These apply to all
130	// operations invoked for this client. Use functional options on operation
131	// call to modify this list for per operation behavior.
132	APIOptions []func(*middleware.Stack) error
133
134	// The endpoint the client will use to retrieve EC2 instance metadata.
135	//
136	// Specifies the EC2 Instance Metadata Service endpoint to use. If specified it overrides EndpointMode.
137	//
138	// If unset, and the environment variable AWS_EC2_METADATA_SERVICE_ENDPOINT
139	// has a value the client will use the value of the environment variable as
140	// the endpoint for operation calls.
141	//
142	//    AWS_EC2_METADATA_SERVICE_ENDPOINT=http://[::1]
143	Endpoint string
144
145	// The endpoint selection mode the client will use if no explicit endpoint is provided using the Endpoint field.
146	//
147	// Setting EndpointMode to EndpointModeStateIPv4 will configure the client to use the default EC2 IPv4 endpoint.
148	// Setting EndpointMode to EndpointModeStateIPv6 will configure the client to use the default EC2 IPv6 endpoint.
149	//
150	// By default if EndpointMode is not set (EndpointModeStateUnset) than the default endpoint selection mode EndpointModeStateIPv4.
151	EndpointMode EndpointModeState
152
153	// The HTTP client to invoke API calls with. Defaults to client's default
154	// HTTP implementation if nil.
155	HTTPClient HTTPClient
156
157	// Retryer guides how HTTP requests should be retried in case of recoverable
158	// failures. When nil the API client will use a default retryer.
159	Retryer aws.Retryer
160
161	// Changes if the EC2 Instance Metadata client is enabled or not. Client
162	// will default to enabled if not set to ClientDisabled. When the client is
163	// disabled it will return an error for all operation calls.
164	//
165	// If ClientEnableState value is ClientDefaultEnableState (default value),
166	// and the environment variable "AWS_EC2_METADATA_DISABLED" is set to
167	// "true", the client will be disabled.
168	//
169	//    AWS_EC2_METADATA_DISABLED=true
170	ClientEnableState ClientEnableState
171
172	// Configures the events that will be sent to the configured logger.
173	ClientLogMode aws.ClientLogMode
174
175	// The logger writer interface to write logging messages to.
176	Logger logging.Logger
177
178	// Configure IMDSv1 fallback behavior. By default, the client will attempt
179	// to fall back to IMDSv1 as needed for backwards compatibility. When set to [aws.FalseTernary]
180	// the client will return any errors encountered from attempting to fetch a token
181	// instead of silently using the insecure data flow of IMDSv1.
182	//
183	// See [configuring IMDS] for more information.
184	//
185	// [configuring IMDS]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
186	EnableFallback aws.Ternary
187
188	// By default, all IMDS client operations enforce a 5-second timeout. You
189	// can disable that behavior with this setting.
190	DisableDefaultTimeout bool
191
192	// provides the caching of API tokens used for operation calls. If unset,
193	// the API token will not be retrieved for the operation.
194	tokenProvider *tokenProvider
195
196	// option to disable the API token provider for testing.
197	disableAPIToken bool
198}
199
200// HTTPClient provides the interface for a client making HTTP requests with the
201// API.
202type HTTPClient interface {
203	Do(*http.Request) (*http.Response, error)
204}
205
206// Copy creates a copy of the API options.
207func (o Options) Copy() Options {
208	to := o
209	to.APIOptions = append([]func(*middleware.Stack) error{}, o.APIOptions...)
210	return to
211}
212
213// WithAPIOptions wraps the API middleware functions, as a functional option
214// for the API Client Options. Use this helper to add additional functional
215// options to the API client, or operation calls.
216func WithAPIOptions(optFns ...func(*middleware.Stack) error) func(*Options) {
217	return func(o *Options) {
218		o.APIOptions = append(o.APIOptions, optFns...)
219	}
220}
221
222func (c *Client) invokeOperation(
223	ctx context.Context, opID string, params interface{}, optFns []func(*Options),
224	stackFns ...func(*middleware.Stack, Options) error,
225) (
226	result interface{}, metadata middleware.Metadata, err error,
227) {
228	stack := middleware.NewStack(opID, smithyhttp.NewStackRequest)
229	options := c.options.Copy()
230	for _, fn := range optFns {
231		fn(&options)
232	}
233
234	if options.ClientEnableState == ClientDisabled {
235		return nil, metadata, &smithy.OperationError{
236			ServiceID:     ServiceID,
237			OperationName: opID,
238			Err: fmt.Errorf(
239				"access disabled to EC2 IMDS via client option, or %q environment variable",
240				disableClientEnvVar),
241		}
242	}
243
244	for _, fn := range stackFns {
245		if err := fn(stack, options); err != nil {
246			return nil, metadata, err
247		}
248	}
249
250	for _, fn := range options.APIOptions {
251		if err := fn(stack); err != nil {
252			return nil, metadata, err
253		}
254	}
255
256	handler := middleware.DecorateHandler(smithyhttp.NewClientHandler(options.HTTPClient), stack)
257	result, metadata, err = handler.Handle(ctx, params)
258	if err != nil {
259		return nil, metadata, &smithy.OperationError{
260			ServiceID:     ServiceID,
261			OperationName: opID,
262			Err:           err,
263		}
264	}
265
266	return result, metadata, err
267}
268
269const (
270	// HTTP client constants
271	defaultDialerTimeout         = 250 * time.Millisecond
272	defaultResponseHeaderTimeout = 500 * time.Millisecond
273)
274
275func resolveHTTPClient(client HTTPClient) HTTPClient {
276	if client == nil {
277		client = awshttp.NewBuildableClient()
278	}
279
280	if c, ok := client.(*awshttp.BuildableClient); ok {
281		client = c.
282			WithDialerOptions(func(d *net.Dialer) {
283				// Use a custom Dial timeout for the EC2 Metadata service to account
284				// for the possibility the application might not be running in an
285				// environment with the service present. The client should fail fast in
286				// this case.
287				d.Timeout = defaultDialerTimeout
288			}).
289			WithTransportOptions(func(tr *http.Transport) {
290				// Use a custom Transport timeout for the EC2 Metadata service to
291				// account for the possibility that the application might be running in
292				// a container, and EC2Metadata service drops the connection after a
293				// single IP Hop. The client should fail fast in this case.
294				tr.ResponseHeaderTimeout = defaultResponseHeaderTimeout
295			})
296	}
297
298	return client
299}
300
301func resolveClientEnableState(cfg aws.Config, options *Options) error {
302	if options.ClientEnableState != ClientDefaultEnableState {
303		return nil
304	}
305	value, found, err := internalconfig.ResolveClientEnableState(cfg.ConfigSources)
306	if err != nil || !found {
307		return err
308	}
309	options.ClientEnableState = value
310	return nil
311}
312
313func resolveEndpointModeConfig(cfg aws.Config, options *Options) error {
314	if options.EndpointMode != EndpointModeStateUnset {
315		return nil
316	}
317	value, found, err := internalconfig.ResolveEndpointModeConfig(cfg.ConfigSources)
318	if err != nil || !found {
319		return err
320	}
321	options.EndpointMode = value
322	return nil
323}
324
325func resolveEndpointConfig(cfg aws.Config, options *Options) error {
326	if len(options.Endpoint) != 0 {
327		return nil
328	}
329	value, found, err := internalconfig.ResolveEndpointConfig(cfg.ConfigSources)
330	if err != nil || !found {
331		return err
332	}
333	options.Endpoint = value
334	return nil
335}
336
337func resolveEnableFallback(cfg aws.Config, options *Options) {
338	if options.EnableFallback != aws.UnknownTernary {
339		return
340	}
341
342	disabled, ok := internalconfig.ResolveV1FallbackDisabled(cfg.ConfigSources)
343	if !ok {
344		return
345	}
346
347	if disabled {
348		options.EnableFallback = aws.FalseTernary
349	} else {
350		options.EnableFallback = aws.TrueTernary
351	}
352}