api_client.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. package imds
  2. import (
  3. "context"
  4. "fmt"
  5. "net"
  6. "net/http"
  7. "os"
  8. "strings"
  9. "time"
  10. "github.com/aws/aws-sdk-go-v2/aws"
  11. "github.com/aws/aws-sdk-go-v2/aws/retry"
  12. awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
  13. internalconfig "github.com/aws/aws-sdk-go-v2/feature/ec2/imds/internal/config"
  14. "github.com/aws/smithy-go"
  15. "github.com/aws/smithy-go/logging"
  16. "github.com/aws/smithy-go/middleware"
  17. smithyhttp "github.com/aws/smithy-go/transport/http"
  18. )
  19. // ServiceID provides the unique name of this API client
  20. const ServiceID = "ec2imds"
  21. // Client provides the API client for interacting with the Amazon EC2 Instance
  22. // Metadata Service API.
  23. type Client struct {
  24. options Options
  25. }
  26. // ClientEnableState provides an enumeration if the client is enabled,
  27. // disabled, or default behavior.
  28. type ClientEnableState = internalconfig.ClientEnableState
  29. // Enumeration values for ClientEnableState
  30. const (
  31. ClientDefaultEnableState ClientEnableState = internalconfig.ClientDefaultEnableState // default behavior
  32. ClientDisabled ClientEnableState = internalconfig.ClientDisabled // client disabled
  33. ClientEnabled ClientEnableState = internalconfig.ClientEnabled // client enabled
  34. )
  35. // EndpointModeState is an enum configuration variable describing the client endpoint mode.
  36. // Not configurable directly, but used when using the NewFromConfig.
  37. type EndpointModeState = internalconfig.EndpointModeState
  38. // Enumeration values for EndpointModeState
  39. const (
  40. EndpointModeStateUnset EndpointModeState = internalconfig.EndpointModeStateUnset
  41. EndpointModeStateIPv4 EndpointModeState = internalconfig.EndpointModeStateIPv4
  42. EndpointModeStateIPv6 EndpointModeState = internalconfig.EndpointModeStateIPv6
  43. )
  44. const (
  45. disableClientEnvVar = "AWS_EC2_METADATA_DISABLED"
  46. // Client endpoint options
  47. endpointEnvVar = "AWS_EC2_METADATA_SERVICE_ENDPOINT"
  48. defaultIPv4Endpoint = "http://169.254.169.254"
  49. defaultIPv6Endpoint = "http://[fd00:ec2::254]"
  50. )
  51. // New returns an initialized Client based on the functional options. Provide
  52. // additional functional options to further configure the behavior of the client,
  53. // such as changing the client's endpoint or adding custom middleware behavior.
  54. func New(options Options, optFns ...func(*Options)) *Client {
  55. options = options.Copy()
  56. for _, fn := range optFns {
  57. fn(&options)
  58. }
  59. options.HTTPClient = resolveHTTPClient(options.HTTPClient)
  60. if options.Retryer == nil {
  61. options.Retryer = retry.NewStandard()
  62. }
  63. options.Retryer = retry.AddWithMaxBackoffDelay(options.Retryer, 1*time.Second)
  64. if options.ClientEnableState == ClientDefaultEnableState {
  65. if v := os.Getenv(disableClientEnvVar); strings.EqualFold(v, "true") {
  66. options.ClientEnableState = ClientDisabled
  67. }
  68. }
  69. if len(options.Endpoint) == 0 {
  70. if v := os.Getenv(endpointEnvVar); len(v) != 0 {
  71. options.Endpoint = v
  72. }
  73. }
  74. client := &Client{
  75. options: options,
  76. }
  77. if client.options.tokenProvider == nil && !client.options.disableAPIToken {
  78. client.options.tokenProvider = newTokenProvider(client, defaultTokenTTL)
  79. }
  80. return client
  81. }
  82. // NewFromConfig returns an initialized Client based the AWS SDK config, and
  83. // functional options. Provide additional functional options to further
  84. // configure the behavior of the client, such as changing the client's endpoint
  85. // or adding custom middleware behavior.
  86. func NewFromConfig(cfg aws.Config, optFns ...func(*Options)) *Client {
  87. opts := Options{
  88. APIOptions: append([]func(*middleware.Stack) error{}, cfg.APIOptions...),
  89. HTTPClient: cfg.HTTPClient,
  90. ClientLogMode: cfg.ClientLogMode,
  91. Logger: cfg.Logger,
  92. }
  93. if cfg.Retryer != nil {
  94. opts.Retryer = cfg.Retryer()
  95. }
  96. resolveClientEnableState(cfg, &opts)
  97. resolveEndpointConfig(cfg, &opts)
  98. resolveEndpointModeConfig(cfg, &opts)
  99. resolveEnableFallback(cfg, &opts)
  100. return New(opts, optFns...)
  101. }
  102. // Options provides the fields for configuring the API client's behavior.
  103. type Options struct {
  104. // Set of options to modify how an operation is invoked. These apply to all
  105. // operations invoked for this client. Use functional options on operation
  106. // call to modify this list for per operation behavior.
  107. APIOptions []func(*middleware.Stack) error
  108. // The endpoint the client will use to retrieve EC2 instance metadata.
  109. //
  110. // Specifies the EC2 Instance Metadata Service endpoint to use. If specified it overrides EndpointMode.
  111. //
  112. // If unset, and the environment variable AWS_EC2_METADATA_SERVICE_ENDPOINT
  113. // has a value the client will use the value of the environment variable as
  114. // the endpoint for operation calls.
  115. //
  116. // AWS_EC2_METADATA_SERVICE_ENDPOINT=http://[::1]
  117. Endpoint string
  118. // The endpoint selection mode the client will use if no explicit endpoint is provided using the Endpoint field.
  119. //
  120. // Setting EndpointMode to EndpointModeStateIPv4 will configure the client to use the default EC2 IPv4 endpoint.
  121. // Setting EndpointMode to EndpointModeStateIPv6 will configure the client to use the default EC2 IPv6 endpoint.
  122. //
  123. // By default if EndpointMode is not set (EndpointModeStateUnset) than the default endpoint selection mode EndpointModeStateIPv4.
  124. EndpointMode EndpointModeState
  125. // The HTTP client to invoke API calls with. Defaults to client's default
  126. // HTTP implementation if nil.
  127. HTTPClient HTTPClient
  128. // Retryer guides how HTTP requests should be retried in case of recoverable
  129. // failures. When nil the API client will use a default retryer.
  130. Retryer aws.Retryer
  131. // Changes if the EC2 Instance Metadata client is enabled or not. Client
  132. // will default to enabled if not set to ClientDisabled. When the client is
  133. // disabled it will return an error for all operation calls.
  134. //
  135. // If ClientEnableState value is ClientDefaultEnableState (default value),
  136. // and the environment variable "AWS_EC2_METADATA_DISABLED" is set to
  137. // "true", the client will be disabled.
  138. //
  139. // AWS_EC2_METADATA_DISABLED=true
  140. ClientEnableState ClientEnableState
  141. // Configures the events that will be sent to the configured logger.
  142. ClientLogMode aws.ClientLogMode
  143. // The logger writer interface to write logging messages to.
  144. Logger logging.Logger
  145. // Configure IMDSv1 fallback behavior. By default, the client will attempt
  146. // to fall back to IMDSv1 as needed for backwards compatibility. When set to [aws.FalseTernary]
  147. // the client will return any errors encountered from attempting to fetch a token
  148. // instead of silently using the insecure data flow of IMDSv1.
  149. //
  150. // See [configuring IMDS] for more information.
  151. //
  152. // [configuring IMDS]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
  153. EnableFallback aws.Ternary
  154. // provides the caching of API tokens used for operation calls. If unset,
  155. // the API token will not be retrieved for the operation.
  156. tokenProvider *tokenProvider
  157. // option to disable the API token provider for testing.
  158. disableAPIToken bool
  159. }
  160. // HTTPClient provides the interface for a client making HTTP requests with the
  161. // API.
  162. type HTTPClient interface {
  163. Do(*http.Request) (*http.Response, error)
  164. }
  165. // Copy creates a copy of the API options.
  166. func (o Options) Copy() Options {
  167. to := o
  168. to.APIOptions = append([]func(*middleware.Stack) error{}, o.APIOptions...)
  169. return to
  170. }
  171. // WithAPIOptions wraps the API middleware functions, as a functional option
  172. // for the API Client Options. Use this helper to add additional functional
  173. // options to the API client, or operation calls.
  174. func WithAPIOptions(optFns ...func(*middleware.Stack) error) func(*Options) {
  175. return func(o *Options) {
  176. o.APIOptions = append(o.APIOptions, optFns...)
  177. }
  178. }
  179. func (c *Client) invokeOperation(
  180. ctx context.Context, opID string, params interface{}, optFns []func(*Options),
  181. stackFns ...func(*middleware.Stack, Options) error,
  182. ) (
  183. result interface{}, metadata middleware.Metadata, err error,
  184. ) {
  185. stack := middleware.NewStack(opID, smithyhttp.NewStackRequest)
  186. options := c.options.Copy()
  187. for _, fn := range optFns {
  188. fn(&options)
  189. }
  190. if options.ClientEnableState == ClientDisabled {
  191. return nil, metadata, &smithy.OperationError{
  192. ServiceID: ServiceID,
  193. OperationName: opID,
  194. Err: fmt.Errorf(
  195. "access disabled to EC2 IMDS via client option, or %q environment variable",
  196. disableClientEnvVar),
  197. }
  198. }
  199. for _, fn := range stackFns {
  200. if err := fn(stack, options); err != nil {
  201. return nil, metadata, err
  202. }
  203. }
  204. for _, fn := range options.APIOptions {
  205. if err := fn(stack); err != nil {
  206. return nil, metadata, err
  207. }
  208. }
  209. handler := middleware.DecorateHandler(smithyhttp.NewClientHandler(options.HTTPClient), stack)
  210. result, metadata, err = handler.Handle(ctx, params)
  211. if err != nil {
  212. return nil, metadata, &smithy.OperationError{
  213. ServiceID: ServiceID,
  214. OperationName: opID,
  215. Err: err,
  216. }
  217. }
  218. return result, metadata, err
  219. }
  220. const (
  221. // HTTP client constants
  222. defaultDialerTimeout = 250 * time.Millisecond
  223. defaultResponseHeaderTimeout = 500 * time.Millisecond
  224. )
  225. func resolveHTTPClient(client HTTPClient) HTTPClient {
  226. if client == nil {
  227. client = awshttp.NewBuildableClient()
  228. }
  229. if c, ok := client.(*awshttp.BuildableClient); ok {
  230. client = c.
  231. WithDialerOptions(func(d *net.Dialer) {
  232. // Use a custom Dial timeout for the EC2 Metadata service to account
  233. // for the possibility the application might not be running in an
  234. // environment with the service present. The client should fail fast in
  235. // this case.
  236. d.Timeout = defaultDialerTimeout
  237. }).
  238. WithTransportOptions(func(tr *http.Transport) {
  239. // Use a custom Transport timeout for the EC2 Metadata service to
  240. // account for the possibility that the application might be running in
  241. // a container, and EC2Metadata service drops the connection after a
  242. // single IP Hop. The client should fail fast in this case.
  243. tr.ResponseHeaderTimeout = defaultResponseHeaderTimeout
  244. })
  245. }
  246. return client
  247. }
  248. func resolveClientEnableState(cfg aws.Config, options *Options) error {
  249. if options.ClientEnableState != ClientDefaultEnableState {
  250. return nil
  251. }
  252. value, found, err := internalconfig.ResolveClientEnableState(cfg.ConfigSources)
  253. if err != nil || !found {
  254. return err
  255. }
  256. options.ClientEnableState = value
  257. return nil
  258. }
  259. func resolveEndpointModeConfig(cfg aws.Config, options *Options) error {
  260. if options.EndpointMode != EndpointModeStateUnset {
  261. return nil
  262. }
  263. value, found, err := internalconfig.ResolveEndpointModeConfig(cfg.ConfigSources)
  264. if err != nil || !found {
  265. return err
  266. }
  267. options.EndpointMode = value
  268. return nil
  269. }
  270. func resolveEndpointConfig(cfg aws.Config, options *Options) error {
  271. if len(options.Endpoint) != 0 {
  272. return nil
  273. }
  274. value, found, err := internalconfig.ResolveEndpointConfig(cfg.ConfigSources)
  275. if err != nil || !found {
  276. return err
  277. }
  278. options.Endpoint = value
  279. return nil
  280. }
  281. func resolveEnableFallback(cfg aws.Config, options *Options) {
  282. if options.EnableFallback != aws.UnknownTernary {
  283. return
  284. }
  285. disabled, ok := internalconfig.ResolveV1FallbackDisabled(cfg.ConfigSources)
  286. if !ok {
  287. return
  288. }
  289. if disabled {
  290. options.EnableFallback = aws.FalseTernary
  291. } else {
  292. options.EnableFallback = aws.TrueTernary
  293. }
  294. }