api_client.go (10227B)
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 } 112 113 if cfg.Retryer != nil { 114 opts.Retryer = cfg.Retryer() 115 } 116 117 resolveClientEnableState(cfg, &opts) 118 resolveEndpointConfig(cfg, &opts) 119 resolveEndpointModeConfig(cfg, &opts) 120 121 return New(opts, optFns...) 122 } 123 124 // Options provides the fields for configuring the API client's behavior. 125 type Options struct { 126 // Set of options to modify how an operation is invoked. These apply to all 127 // operations invoked for this client. Use functional options on operation 128 // call to modify this list for per operation behavior. 129 APIOptions []func(*middleware.Stack) error 130 131 // The endpoint the client will use to retrieve EC2 instance metadata. 132 // 133 // Specifies the EC2 Instance Metadata Service endpoint to use. If specified it overrides EndpointMode. 134 // 135 // If unset, and the environment variable AWS_EC2_METADATA_SERVICE_ENDPOINT 136 // has a value the client will use the value of the environment variable as 137 // the endpoint for operation calls. 138 // 139 // AWS_EC2_METADATA_SERVICE_ENDPOINT=http://[::1] 140 Endpoint string 141 142 // The endpoint selection mode the client will use if no explicit endpoint is provided using the Endpoint field. 143 // 144 // Setting EndpointMode to EndpointModeStateIPv4 will configure the client to use the default EC2 IPv4 endpoint. 145 // Setting EndpointMode to EndpointModeStateIPv6 will configure the client to use the default EC2 IPv6 endpoint. 146 // 147 // By default if EndpointMode is not set (EndpointModeStateUnset) than the default endpoint selection mode EndpointModeStateIPv4. 148 EndpointMode EndpointModeState 149 150 // The HTTP client to invoke API calls with. Defaults to client's default 151 // HTTP implementation if nil. 152 HTTPClient HTTPClient 153 154 // Retryer guides how HTTP requests should be retried in case of recoverable 155 // failures. When nil the API client will use a default retryer. 156 Retryer aws.Retryer 157 158 // Changes if the EC2 Instance Metadata client is enabled or not. Client 159 // will default to enabled if not set to ClientDisabled. When the client is 160 // disabled it will return an error for all operation calls. 161 // 162 // If ClientEnableState value is ClientDefaultEnableState (default value), 163 // and the environment variable "AWS_EC2_METADATA_DISABLED" is set to 164 // "true", the client will be disabled. 165 // 166 // AWS_EC2_METADATA_DISABLED=true 167 ClientEnableState ClientEnableState 168 169 // Configures the events that will be sent to the configured logger. 170 ClientLogMode aws.ClientLogMode 171 172 // The logger writer interface to write logging messages to. 173 Logger logging.Logger 174 175 // provides the caching of API tokens used for operation calls. If unset, 176 // the API token will not be retrieved for the operation. 177 tokenProvider *tokenProvider 178 179 // option to disable the API token provider for testing. 180 disableAPIToken bool 181 } 182 183 // HTTPClient provides the interface for a client making HTTP requests with the 184 // API. 185 type HTTPClient interface { 186 Do(*http.Request) (*http.Response, error) 187 } 188 189 // Copy creates a copy of the API options. 190 func (o Options) Copy() Options { 191 to := o 192 to.APIOptions = append([]func(*middleware.Stack) error{}, o.APIOptions...) 193 return to 194 } 195 196 // WithAPIOptions wraps the API middleware functions, as a functional option 197 // for the API Client Options. Use this helper to add additional functional 198 // options to the API client, or operation calls. 199 func WithAPIOptions(optFns ...func(*middleware.Stack) error) func(*Options) { 200 return func(o *Options) { 201 o.APIOptions = append(o.APIOptions, optFns...) 202 } 203 } 204 205 func (c *Client) invokeOperation( 206 ctx context.Context, opID string, params interface{}, optFns []func(*Options), 207 stackFns ...func(*middleware.Stack, Options) error, 208 ) ( 209 result interface{}, metadata middleware.Metadata, err error, 210 ) { 211 stack := middleware.NewStack(opID, smithyhttp.NewStackRequest) 212 options := c.options.Copy() 213 for _, fn := range optFns { 214 fn(&options) 215 } 216 217 if options.ClientEnableState == ClientDisabled { 218 return nil, metadata, &smithy.OperationError{ 219 ServiceID: ServiceID, 220 OperationName: opID, 221 Err: fmt.Errorf( 222 "access disabled to EC2 IMDS via client option, or %q environment variable", 223 disableClientEnvVar), 224 } 225 } 226 227 for _, fn := range stackFns { 228 if err := fn(stack, options); err != nil { 229 return nil, metadata, err 230 } 231 } 232 233 for _, fn := range options.APIOptions { 234 if err := fn(stack); err != nil { 235 return nil, metadata, err 236 } 237 } 238 239 handler := middleware.DecorateHandler(smithyhttp.NewClientHandler(options.HTTPClient), stack) 240 result, metadata, err = handler.Handle(ctx, params) 241 if err != nil { 242 return nil, metadata, &smithy.OperationError{ 243 ServiceID: ServiceID, 244 OperationName: opID, 245 Err: err, 246 } 247 } 248 249 return result, metadata, err 250 } 251 252 const ( 253 // HTTP client constants 254 defaultDialerTimeout = 250 * time.Millisecond 255 defaultResponseHeaderTimeout = 500 * time.Millisecond 256 ) 257 258 func resolveHTTPClient(client HTTPClient) HTTPClient { 259 if client == nil { 260 client = awshttp.NewBuildableClient() 261 } 262 263 if c, ok := client.(*awshttp.BuildableClient); ok { 264 client = c. 265 WithDialerOptions(func(d *net.Dialer) { 266 // Use a custom Dial timeout for the EC2 Metadata service to account 267 // for the possibility the application might not be running in an 268 // environment with the service present. The client should fail fast in 269 // this case. 270 d.Timeout = defaultDialerTimeout 271 }). 272 WithTransportOptions(func(tr *http.Transport) { 273 // Use a custom Transport timeout for the EC2 Metadata service to 274 // account for the possibility that the application might be running in 275 // a container, and EC2Metadata service drops the connection after a 276 // single IP Hop. The client should fail fast in this case. 277 tr.ResponseHeaderTimeout = defaultResponseHeaderTimeout 278 }) 279 } 280 281 return client 282 } 283 284 func resolveClientEnableState(cfg aws.Config, options *Options) error { 285 if options.ClientEnableState != ClientDefaultEnableState { 286 return nil 287 } 288 value, found, err := internalconfig.ResolveClientEnableState(cfg.ConfigSources) 289 if err != nil || !found { 290 return err 291 } 292 options.ClientEnableState = value 293 return nil 294 } 295 296 func resolveEndpointModeConfig(cfg aws.Config, options *Options) error { 297 if options.EndpointMode != EndpointModeStateUnset { 298 return nil 299 } 300 value, found, err := internalconfig.ResolveEndpointModeConfig(cfg.ConfigSources) 301 if err != nil || !found { 302 return err 303 } 304 options.EndpointMode = value 305 return nil 306 } 307 308 func resolveEndpointConfig(cfg aws.Config, options *Options) error { 309 if len(options.Endpoint) != 0 { 310 return nil 311 } 312 value, found, err := internalconfig.ResolveEndpointConfig(cfg.ConfigSources) 313 if err != nil || !found { 314 return err 315 } 316 options.Endpoint = value 317 return nil 318 }