src

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

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 }