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}