src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

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 }