api_client.go raw

   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  }
 359