api_client.go (11570B)
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 }