code.dwrz.net

Go monorepo.
Log | Files | Refs

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 }