api_client.go (11203B)
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 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. 107 func 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. 128 type 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 // provides the caching of API tokens used for operation calls. If unset, 189 // the API token will not be retrieved for the operation. 190 tokenProvider *tokenProvider 191 192 // option to disable the API token provider for testing. 193 disableAPIToken bool 194 } 195 196 // HTTPClient provides the interface for a client making HTTP requests with the 197 // API. 198 type HTTPClient interface { 199 Do(*http.Request) (*http.Response, error) 200 } 201 202 // Copy creates a copy of the API options. 203 func (o Options) Copy() Options { 204 to := o 205 to.APIOptions = append([]func(*middleware.Stack) error{}, o.APIOptions...) 206 return to 207 } 208 209 // WithAPIOptions wraps the API middleware functions, as a functional option 210 // for the API Client Options. Use this helper to add additional functional 211 // options to the API client, or operation calls. 212 func WithAPIOptions(optFns ...func(*middleware.Stack) error) func(*Options) { 213 return func(o *Options) { 214 o.APIOptions = append(o.APIOptions, optFns...) 215 } 216 } 217 218 func (c *Client) invokeOperation( 219 ctx context.Context, opID string, params interface{}, optFns []func(*Options), 220 stackFns ...func(*middleware.Stack, Options) error, 221 ) ( 222 result interface{}, metadata middleware.Metadata, err error, 223 ) { 224 stack := middleware.NewStack(opID, smithyhttp.NewStackRequest) 225 options := c.options.Copy() 226 for _, fn := range optFns { 227 fn(&options) 228 } 229 230 if options.ClientEnableState == ClientDisabled { 231 return nil, metadata, &smithy.OperationError{ 232 ServiceID: ServiceID, 233 OperationName: opID, 234 Err: fmt.Errorf( 235 "access disabled to EC2 IMDS via client option, or %q environment variable", 236 disableClientEnvVar), 237 } 238 } 239 240 for _, fn := range stackFns { 241 if err := fn(stack, options); err != nil { 242 return nil, metadata, err 243 } 244 } 245 246 for _, fn := range options.APIOptions { 247 if err := fn(stack); err != nil { 248 return nil, metadata, err 249 } 250 } 251 252 handler := middleware.DecorateHandler(smithyhttp.NewClientHandler(options.HTTPClient), stack) 253 result, metadata, err = handler.Handle(ctx, params) 254 if err != nil { 255 return nil, metadata, &smithy.OperationError{ 256 ServiceID: ServiceID, 257 OperationName: opID, 258 Err: err, 259 } 260 } 261 262 return result, metadata, err 263 } 264 265 const ( 266 // HTTP client constants 267 defaultDialerTimeout = 250 * time.Millisecond 268 defaultResponseHeaderTimeout = 500 * time.Millisecond 269 ) 270 271 func resolveHTTPClient(client HTTPClient) HTTPClient { 272 if client == nil { 273 client = awshttp.NewBuildableClient() 274 } 275 276 if c, ok := client.(*awshttp.BuildableClient); ok { 277 client = c. 278 WithDialerOptions(func(d *net.Dialer) { 279 // Use a custom Dial timeout for the EC2 Metadata service to account 280 // for the possibility the application might not be running in an 281 // environment with the service present. The client should fail fast in 282 // this case. 283 d.Timeout = defaultDialerTimeout 284 }). 285 WithTransportOptions(func(tr *http.Transport) { 286 // Use a custom Transport timeout for the EC2 Metadata service to 287 // account for the possibility that the application might be running in 288 // a container, and EC2Metadata service drops the connection after a 289 // single IP Hop. The client should fail fast in this case. 290 tr.ResponseHeaderTimeout = defaultResponseHeaderTimeout 291 }) 292 } 293 294 return client 295 } 296 297 func resolveClientEnableState(cfg aws.Config, options *Options) error { 298 if options.ClientEnableState != ClientDefaultEnableState { 299 return nil 300 } 301 value, found, err := internalconfig.ResolveClientEnableState(cfg.ConfigSources) 302 if err != nil || !found { 303 return err 304 } 305 options.ClientEnableState = value 306 return nil 307 } 308 309 func resolveEndpointModeConfig(cfg aws.Config, options *Options) error { 310 if options.EndpointMode != EndpointModeStateUnset { 311 return nil 312 } 313 value, found, err := internalconfig.ResolveEndpointModeConfig(cfg.ConfigSources) 314 if err != nil || !found { 315 return err 316 } 317 options.EndpointMode = value 318 return nil 319 } 320 321 func resolveEndpointConfig(cfg aws.Config, options *Options) error { 322 if len(options.Endpoint) != 0 { 323 return nil 324 } 325 value, found, err := internalconfig.ResolveEndpointConfig(cfg.ConfigSources) 326 if err != nil || !found { 327 return err 328 } 329 options.Endpoint = value 330 return nil 331 } 332 333 func resolveEnableFallback(cfg aws.Config, options *Options) { 334 if options.EnableFallback != aws.UnknownTernary { 335 return 336 } 337 338 disabled, ok := internalconfig.ResolveV1FallbackDisabled(cfg.ConfigSources) 339 if !ok { 340 return 341 } 342 343 if disabled { 344 options.EnableFallback = aws.FalseTernary 345 } else { 346 options.EnableFallback = aws.TrueTernary 347 } 348 }