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