1# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"). You
4# may not use this file except in compliance with the License. A copy of
5# the License is located at
6#
7# http://aws.amazon.com/apache2.0/
8#
9# or in the "license" file accompanying this file. This file is
10# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11# ANY KIND, either express or implied. See the License for the specific
12# language governing permissions and limitations under the License.
13"""This module contains the interface for controlling how configuration
14is loaded.
15"""
16
17import copy
18import logging
19import os
20
21from botocore import utils
22from botocore.exceptions import InvalidConfigError
23
24logger = logging.getLogger(__name__)
25
26
27#: A default dictionary that maps the logical names for session variables
28#: to the specific environment variables and configuration file names
29#: that contain the values for these variables.
30#: When creating a new Session object, you can pass in your own dictionary
31#: to remap the logical names or to add new logical names. You can then
32#: get the current value for these variables by using the
33#: ``get_config_variable`` method of the :class:`botocore.session.Session`
34#: class.
35#: These form the keys of the dictionary. The values in the dictionary
36#: are tuples of (<config_name>, <environment variable>, <default value>,
37#: <conversion func>).
38#: The conversion func is a function that takes the configuration value
39#: as an argument and returns the converted value. If this value is
40#: None, then the configuration value is returned unmodified. This
41#: conversion function can be used to type convert config values to
42#: values other than the default values of strings.
43#: The ``profile`` and ``config_file`` variables should always have a
44#: None value for the first entry in the tuple because it doesn't make
45#: sense to look inside the config file for the location of the config
46#: file or for the default profile to use.
47#: The ``config_name`` is the name to look for in the configuration file,
48#: the ``env var`` is the OS environment variable (``os.environ``) to
49#: use, and ``default_value`` is the value to use if no value is otherwise
50#: found.
51#: NOTE: Fixing the spelling of this variable would be a breaking change.
52#: Please leave as is.
53BOTOCORE_DEFAUT_SESSION_VARIABLES = {
54 # logical: config_file, env_var, default_value, conversion_func
55 'profile': (None, ['AWS_DEFAULT_PROFILE', 'AWS_PROFILE'], None, None),
56 'region': ('region', 'AWS_DEFAULT_REGION', None, None),
57 'data_path': ('data_path', 'AWS_DATA_PATH', None, None),
58 'config_file': (None, 'AWS_CONFIG_FILE', '~/.aws/config', None),
59 'ca_bundle': ('ca_bundle', 'AWS_CA_BUNDLE', None, None),
60 'api_versions': ('api_versions', None, {}, None),
61 # This is the shared credentials file amongst sdks.
62 'credentials_file': (
63 None,
64 'AWS_SHARED_CREDENTIALS_FILE',
65 '~/.aws/credentials',
66 None,
67 ),
68 # These variables only exist in the config file.
69 # This is the number of seconds until we time out a request to
70 # the instance metadata service.
71 'metadata_service_timeout': (
72 'metadata_service_timeout',
73 'AWS_METADATA_SERVICE_TIMEOUT',
74 1,
75 int,
76 ),
77 # This is the number of request attempts we make until we give
78 # up trying to retrieve data from the instance metadata service.
79 'metadata_service_num_attempts': (
80 'metadata_service_num_attempts',
81 'AWS_METADATA_SERVICE_NUM_ATTEMPTS',
82 1,
83 int,
84 ),
85 'ec2_metadata_service_endpoint': (
86 'ec2_metadata_service_endpoint',
87 'AWS_EC2_METADATA_SERVICE_ENDPOINT',
88 None,
89 None,
90 ),
91 'ec2_metadata_service_endpoint_mode': (
92 'ec2_metadata_service_endpoint_mode',
93 'AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE',
94 None,
95 None,
96 ),
97 'ec2_metadata_v1_disabled': (
98 'ec2_metadata_v1_disabled',
99 'AWS_EC2_METADATA_V1_DISABLED',
100 False,
101 utils.ensure_boolean,
102 ),
103 'imds_use_ipv6': (
104 'imds_use_ipv6',
105 'AWS_IMDS_USE_IPV6',
106 False,
107 utils.ensure_boolean,
108 ),
109 'use_dualstack_endpoint': (
110 'use_dualstack_endpoint',
111 'AWS_USE_DUALSTACK_ENDPOINT',
112 None,
113 utils.ensure_boolean,
114 ),
115 'use_fips_endpoint': (
116 'use_fips_endpoint',
117 'AWS_USE_FIPS_ENDPOINT',
118 None,
119 utils.ensure_boolean,
120 ),
121 'ignore_configured_endpoint_urls': (
122 'ignore_configured_endpoint_urls',
123 'AWS_IGNORE_CONFIGURED_ENDPOINT_URLS',
124 None,
125 utils.ensure_boolean,
126 ),
127 'parameter_validation': ('parameter_validation', None, True, None),
128 # Client side monitoring configurations.
129 # Note: These configurations are considered internal to botocore.
130 # Do not use them until publicly documented.
131 'csm_enabled': (
132 'csm_enabled',
133 'AWS_CSM_ENABLED',
134 False,
135 utils.ensure_boolean,
136 ),
137 'csm_host': ('csm_host', 'AWS_CSM_HOST', '127.0.0.1', None),
138 'csm_port': ('csm_port', 'AWS_CSM_PORT', 31000, int),
139 'csm_client_id': ('csm_client_id', 'AWS_CSM_CLIENT_ID', '', None),
140 # Endpoint discovery configuration
141 'endpoint_discovery_enabled': (
142 'endpoint_discovery_enabled',
143 'AWS_ENDPOINT_DISCOVERY_ENABLED',
144 'auto',
145 None,
146 ),
147 'retry_mode': ('retry_mode', 'AWS_RETRY_MODE', 'legacy', None),
148 'defaults_mode': ('defaults_mode', 'AWS_DEFAULTS_MODE', 'legacy', None),
149 # We can't have a default here for v1 because we need to defer to
150 # whatever the defaults are in _retry.json.
151 'max_attempts': ('max_attempts', 'AWS_MAX_ATTEMPTS', None, int),
152 'user_agent_appid': ('sdk_ua_app_id', 'AWS_SDK_UA_APP_ID', None, None),
153 'request_min_compression_size_bytes': (
154 'request_min_compression_size_bytes',
155 'AWS_REQUEST_MIN_COMPRESSION_SIZE_BYTES',
156 10240,
157 None,
158 ),
159 'disable_request_compression': (
160 'disable_request_compression',
161 'AWS_DISABLE_REQUEST_COMPRESSION',
162 False,
163 utils.ensure_boolean,
164 ),
165 'sigv4a_signing_region_set': (
166 'sigv4a_signing_region_set',
167 'AWS_SIGV4A_SIGNING_REGION_SET',
168 None,
169 None,
170 ),
171}
172
173# Evaluate AWS_STS_REGIONAL_ENDPOINTS settings
174try:
175 # This is not a public interface and is subject to abrupt breaking changes.
176 # Any usage is not advised or supported in external code bases.
177 from botocore.customizations.sts import (
178 sts_default_setting as _sts_default_setting,
179 )
180except ImportError:
181 _sts_default_setting = 'legacy'
182
183_STS_DEFAULT_SETTINGS = {
184 'sts_regional_endpoints': (
185 'sts_regional_endpoints',
186 'AWS_STS_REGIONAL_ENDPOINTS',
187 _sts_default_setting,
188 None,
189 ),
190}
191BOTOCORE_DEFAUT_SESSION_VARIABLES.update(_STS_DEFAULT_SETTINGS)
192
193
194# A mapping for the s3 specific configuration vars. These are the configuration
195# vars that typically go in the s3 section of the config file. This mapping
196# follows the same schema as the previous session variable mapping.
197DEFAULT_S3_CONFIG_VARS = {
198 'addressing_style': (('s3', 'addressing_style'), None, None, None),
199 'use_accelerate_endpoint': (
200 ('s3', 'use_accelerate_endpoint'),
201 None,
202 None,
203 utils.ensure_boolean,
204 ),
205 'use_dualstack_endpoint': (
206 ('s3', 'use_dualstack_endpoint'),
207 None,
208 None,
209 utils.ensure_boolean,
210 ),
211 'payload_signing_enabled': (
212 ('s3', 'payload_signing_enabled'),
213 None,
214 None,
215 utils.ensure_boolean,
216 ),
217 'use_arn_region': (
218 ['s3_use_arn_region', ('s3', 'use_arn_region')],
219 'AWS_S3_USE_ARN_REGION',
220 None,
221 utils.ensure_boolean,
222 ),
223 'us_east_1_regional_endpoint': (
224 [
225 's3_us_east_1_regional_endpoint',
226 ('s3', 'us_east_1_regional_endpoint'),
227 ],
228 'AWS_S3_US_EAST_1_REGIONAL_ENDPOINT',
229 None,
230 None,
231 ),
232 's3_disable_multiregion_access_points': (
233 ('s3', 's3_disable_multiregion_access_points'),
234 'AWS_S3_DISABLE_MULTIREGION_ACCESS_POINTS',
235 None,
236 utils.ensure_boolean,
237 ),
238}
239# A mapping for the proxy specific configuration vars. These are
240# used to configure how botocore interacts with proxy setups while
241# sending requests.
242DEFAULT_PROXIES_CONFIG_VARS = {
243 'proxy_ca_bundle': ('proxy_ca_bundle', None, None, None),
244 'proxy_client_cert': ('proxy_client_cert', None, None, None),
245 'proxy_use_forwarding_for_https': (
246 'proxy_use_forwarding_for_https',
247 None,
248 None,
249 utils.normalize_boolean,
250 ),
251}
252
253
254def create_botocore_default_config_mapping(session):
255 chain_builder = ConfigChainFactory(session=session)
256 config_mapping = _create_config_chain_mapping(
257 chain_builder, BOTOCORE_DEFAUT_SESSION_VARIABLES
258 )
259 config_mapping['s3'] = SectionConfigProvider(
260 's3',
261 session,
262 _create_config_chain_mapping(chain_builder, DEFAULT_S3_CONFIG_VARS),
263 )
264 config_mapping['proxies_config'] = SectionConfigProvider(
265 'proxies_config',
266 session,
267 _create_config_chain_mapping(
268 chain_builder, DEFAULT_PROXIES_CONFIG_VARS
269 ),
270 )
271 return config_mapping
272
273
274def _create_config_chain_mapping(chain_builder, config_variables):
275 mapping = {}
276 for logical_name, config in config_variables.items():
277 mapping[logical_name] = chain_builder.create_config_chain(
278 instance_name=logical_name,
279 env_var_names=config[1],
280 config_property_names=config[0],
281 default=config[2],
282 conversion_func=config[3],
283 )
284 return mapping
285
286
287class DefaultConfigResolver:
288 def __init__(self, default_config_data):
289 self._base_default_config = default_config_data['base']
290 self._modes = default_config_data['modes']
291 self._resolved_default_configurations = {}
292
293 def _resolve_default_values_by_mode(self, mode):
294 default_config = self._base_default_config.copy()
295 modifications = self._modes.get(mode)
296
297 for config_var in modifications:
298 default_value = default_config[config_var]
299 modification_dict = modifications[config_var]
300 modification = list(modification_dict.keys())[0]
301 modification_value = modification_dict[modification]
302 if modification == 'multiply':
303 default_value *= modification_value
304 elif modification == 'add':
305 default_value += modification_value
306 elif modification == 'override':
307 default_value = modification_value
308 default_config[config_var] = default_value
309 return default_config
310
311 def get_default_modes(self):
312 default_modes = ['legacy', 'auto']
313 default_modes.extend(self._modes.keys())
314 return default_modes
315
316 def get_default_config_values(self, mode):
317 if mode not in self._resolved_default_configurations:
318 defaults = self._resolve_default_values_by_mode(mode)
319 self._resolved_default_configurations[mode] = defaults
320 return self._resolved_default_configurations[mode]
321
322
323class ConfigChainFactory:
324 """Factory class to create our most common configuration chain case.
325
326 This is a convenience class to construct configuration chains that follow
327 our most common pattern. This is to prevent ordering them incorrectly,
328 and to make the config chain construction more readable.
329 """
330
331 def __init__(self, session, environ=None):
332 """Initialize a ConfigChainFactory.
333
334 :type session: :class:`botocore.session.Session`
335 :param session: This is the session that should be used to look up
336 values from the config file.
337
338 :type environ: dict
339 :param environ: A mapping to use for environment variables. If this
340 is not provided it will default to use os.environ.
341 """
342 self._session = session
343 if environ is None:
344 environ = os.environ
345 self._environ = environ
346
347 def create_config_chain(
348 self,
349 instance_name=None,
350 env_var_names=None,
351 config_property_names=None,
352 default=None,
353 conversion_func=None,
354 ):
355 """Build a config chain following the standard botocore pattern.
356
357 In botocore most of our config chains follow the the precendence:
358 session_instance_variables, environment, config_file, default_value.
359
360 This is a convenience function for creating a chain that follow
361 that precendence.
362
363 :type instance_name: str
364 :param instance_name: This indicates what session instance variable
365 corresponds to this config value. If it is None it will not be
366 added to the chain.
367
368 :type env_var_names: str or list of str or None
369 :param env_var_names: One or more environment variable names to
370 search for this value. They are searched in order. If it is None
371 it will not be added to the chain.
372
373 :type config_property_names: str/tuple or list of str/tuple or None
374 :param config_property_names: One of more strings or tuples
375 representing the name of the key in the config file for this
376 config option. They are searched in order. If it is None it will
377 not be added to the chain.
378
379 :type default: Any
380 :param default: Any constant value to be returned.
381
382 :type conversion_func: None or callable
383 :param conversion_func: If this value is None then it has no effect on
384 the return type. Otherwise, it is treated as a function that will
385 conversion_func our provided type.
386
387 :rvalue: ConfigChain
388 :returns: A ConfigChain that resolves in the order env_var_names ->
389 config_property_name -> default. Any values that were none are
390 omitted form the chain.
391 """
392 providers = []
393 if instance_name is not None:
394 providers.append(
395 InstanceVarProvider(
396 instance_var=instance_name, session=self._session
397 )
398 )
399 if env_var_names is not None:
400 providers.extend(self._get_env_providers(env_var_names))
401 if config_property_names is not None:
402 providers.extend(
403 self._get_scoped_config_providers(config_property_names)
404 )
405 if default is not None:
406 providers.append(ConstantProvider(value=default))
407
408 return ChainProvider(
409 providers=providers,
410 conversion_func=conversion_func,
411 )
412
413 def _get_env_providers(self, env_var_names):
414 env_var_providers = []
415 if not isinstance(env_var_names, list):
416 env_var_names = [env_var_names]
417 for env_var_name in env_var_names:
418 env_var_providers.append(
419 EnvironmentProvider(name=env_var_name, env=self._environ)
420 )
421 return env_var_providers
422
423 def _get_scoped_config_providers(self, config_property_names):
424 scoped_config_providers = []
425 if not isinstance(config_property_names, list):
426 config_property_names = [config_property_names]
427 for config_property_name in config_property_names:
428 scoped_config_providers.append(
429 ScopedConfigProvider(
430 config_var_name=config_property_name,
431 session=self._session,
432 )
433 )
434 return scoped_config_providers
435
436
437class ConfigValueStore:
438 """The ConfigValueStore object stores configuration values."""
439
440 def __init__(self, mapping=None):
441 """Initialize a ConfigValueStore.
442
443 :type mapping: dict
444 :param mapping: The mapping parameter is a map of string to a subclass
445 of BaseProvider. When a config variable is asked for via the
446 get_config_variable method, the corresponding provider will be
447 invoked to load the value.
448 """
449 self._overrides = {}
450 self._mapping = {}
451 if mapping is not None:
452 for logical_name, provider in mapping.items():
453 self.set_config_provider(logical_name, provider)
454
455 def __deepcopy__(self, memo):
456 config_store = ConfigValueStore(copy.deepcopy(self._mapping, memo))
457 for logical_name, override_value in self._overrides.items():
458 config_store.set_config_variable(logical_name, override_value)
459
460 return config_store
461
462 def __copy__(self):
463 config_store = ConfigValueStore(copy.copy(self._mapping))
464 for logical_name, override_value in self._overrides.items():
465 config_store.set_config_variable(logical_name, override_value)
466
467 return config_store
468
469 def get_config_variable(self, logical_name):
470 """
471 Retrieve the value associeated with the specified logical_name
472 from the corresponding provider. If no value is found None will
473 be returned.
474
475 :type logical_name: str
476 :param logical_name: The logical name of the session variable
477 you want to retrieve. This name will be mapped to the
478 appropriate environment variable name for this session as
479 well as the appropriate config file entry.
480
481 :returns: value of variable or None if not defined.
482 """
483 if logical_name in self._overrides:
484 return self._overrides[logical_name]
485 if logical_name not in self._mapping:
486 return None
487 provider = self._mapping[logical_name]
488 return provider.provide()
489
490 def get_config_provider(self, logical_name):
491 """
492 Retrieve the provider associated with the specified logical_name.
493 If no provider is found None will be returned.
494
495 :type logical_name: str
496 :param logical_name: The logical name of the session variable
497 you want to retrieve. This name will be mapped to the
498 appropriate environment variable name for this session as
499 well as the appropriate config file entry.
500
501 :returns: configuration provider or None if not defined.
502 """
503 if (
504 logical_name in self._overrides
505 or logical_name not in self._mapping
506 ):
507 return None
508 provider = self._mapping[logical_name]
509 return provider
510
511 def set_config_variable(self, logical_name, value):
512 """Set a configuration variable to a specific value.
513
514 By using this method, you can override the normal lookup
515 process used in ``get_config_variable`` by explicitly setting
516 a value. Subsequent calls to ``get_config_variable`` will
517 use the ``value``. This gives you per-session specific
518 configuration values.
519
520 ::
521 >>> # Assume logical name 'foo' maps to env var 'FOO'
522 >>> os.environ['FOO'] = 'myvalue'
523 >>> s.get_config_variable('foo')
524 'myvalue'
525 >>> s.set_config_variable('foo', 'othervalue')
526 >>> s.get_config_variable('foo')
527 'othervalue'
528
529 :type logical_name: str
530 :param logical_name: The logical name of the session variable
531 you want to set. These are the keys in ``SESSION_VARIABLES``.
532
533 :param value: The value to associate with the config variable.
534 """
535 self._overrides[logical_name] = value
536
537 def clear_config_variable(self, logical_name):
538 """Remove an override config variable from the session.
539
540 :type logical_name: str
541 :param logical_name: The name of the parameter to clear the override
542 value from.
543 """
544 self._overrides.pop(logical_name, None)
545
546 def set_config_provider(self, logical_name, provider):
547 """Set the provider for a config value.
548
549 This provides control over how a particular configuration value is
550 loaded. This replaces the provider for ``logical_name`` with the new
551 ``provider``.
552
553 :type logical_name: str
554 :param logical_name: The name of the config value to change the config
555 provider for.
556
557 :type provider: :class:`botocore.configprovider.BaseProvider`
558 :param provider: The new provider that should be responsible for
559 providing a value for the config named ``logical_name``.
560 """
561 self._mapping[logical_name] = provider
562
563
564class SmartDefaultsConfigStoreFactory:
565 def __init__(self, default_config_resolver, imds_region_provider):
566 self._default_config_resolver = default_config_resolver
567 self._imds_region_provider = imds_region_provider
568 # Initializing _instance_metadata_region as None so we
569 # can fetch region in a lazy fashion only when needed.
570 self._instance_metadata_region = None
571
572 def merge_smart_defaults(self, config_store, mode, region_name):
573 if mode == 'auto':
574 mode = self.resolve_auto_mode(region_name)
575 default_configs = (
576 self._default_config_resolver.get_default_config_values(mode)
577 )
578 for config_var in default_configs:
579 config_value = default_configs[config_var]
580 method = getattr(self, f'_set_{config_var}', None)
581 if method:
582 method(config_store, config_value)
583
584 def resolve_auto_mode(self, region_name):
585 current_region = None
586 if os.environ.get('AWS_EXECUTION_ENV'):
587 default_region = os.environ.get('AWS_DEFAULT_REGION')
588 current_region = os.environ.get('AWS_REGION', default_region)
589 if not current_region:
590 if self._instance_metadata_region:
591 current_region = self._instance_metadata_region
592 else:
593 try:
594 current_region = self._imds_region_provider.provide()
595 self._instance_metadata_region = current_region
596 except Exception:
597 pass
598
599 if current_region:
600 if region_name == current_region:
601 return 'in-region'
602 else:
603 return 'cross-region'
604 return 'standard'
605
606 def _update_provider(self, config_store, variable, value):
607 original_provider = config_store.get_config_provider(variable)
608 default_provider = ConstantProvider(value)
609 if isinstance(original_provider, ChainProvider):
610 chain_provider_copy = copy.deepcopy(original_provider)
611 chain_provider_copy.set_default_provider(default_provider)
612 default_provider = chain_provider_copy
613 elif isinstance(original_provider, BaseProvider):
614 default_provider = ChainProvider(
615 providers=[original_provider, default_provider]
616 )
617 config_store.set_config_provider(variable, default_provider)
618
619 def _update_section_provider(
620 self, config_store, section_name, variable, value
621 ):
622 section_provider_copy = copy.deepcopy(
623 config_store.get_config_provider(section_name)
624 )
625 section_provider_copy.set_default_provider(
626 variable, ConstantProvider(value)
627 )
628 config_store.set_config_provider(section_name, section_provider_copy)
629
630 def _set_retryMode(self, config_store, value):
631 self._update_provider(config_store, 'retry_mode', value)
632
633 def _set_stsRegionalEndpoints(self, config_store, value):
634 self._update_provider(config_store, 'sts_regional_endpoints', value)
635
636 def _set_s3UsEast1RegionalEndpoints(self, config_store, value):
637 self._update_section_provider(
638 config_store, 's3', 'us_east_1_regional_endpoint', value
639 )
640
641 def _set_connectTimeoutInMillis(self, config_store, value):
642 self._update_provider(config_store, 'connect_timeout', value / 1000)
643
644
645class BaseProvider:
646 """Base class for configuration value providers.
647
648 A configuration provider has some method of providing a configuration
649 value.
650 """
651
652 def provide(self):
653 """Provide a config value."""
654 raise NotImplementedError('provide')
655
656
657class ChainProvider(BaseProvider):
658 """This provider wraps one or more other providers.
659
660 Each provider in the chain is called, the first one returning a non-None
661 value is then returned.
662 """
663
664 def __init__(self, providers=None, conversion_func=None):
665 """Initalize a ChainProvider.
666
667 :type providers: list
668 :param providers: The initial list of providers to check for values
669 when invoked.
670
671 :type conversion_func: None or callable
672 :param conversion_func: If this value is None then it has no affect on
673 the return type. Otherwise, it is treated as a function that will
674 transform provided value.
675 """
676 if providers is None:
677 providers = []
678 self._providers = providers
679 self._conversion_func = conversion_func
680
681 def __deepcopy__(self, memo):
682 return ChainProvider(
683 copy.deepcopy(self._providers, memo), self._conversion_func
684 )
685
686 def provide(self):
687 """Provide the value from the first provider to return non-None.
688
689 Each provider in the chain has its provide method called. The first
690 one in the chain to return a non-None value is the returned from the
691 ChainProvider. When no non-None value is found, None is returned.
692 """
693 for provider in self._providers:
694 value = provider.provide()
695 if value is not None:
696 return self._convert_type(value)
697 return None
698
699 def set_default_provider(self, default_provider):
700 if self._providers and isinstance(
701 self._providers[-1], ConstantProvider
702 ):
703 self._providers[-1] = default_provider
704 else:
705 self._providers.append(default_provider)
706
707 num_of_constants = sum(
708 isinstance(provider, ConstantProvider)
709 for provider in self._providers
710 )
711 if num_of_constants > 1:
712 logger.info(
713 'ChainProvider object contains multiple '
714 'instances of ConstantProvider objects'
715 )
716
717 def _convert_type(self, value):
718 if self._conversion_func is not None:
719 return self._conversion_func(value)
720 return value
721
722 def __repr__(self):
723 return '[{}]'.format(', '.join([str(p) for p in self._providers]))
724
725
726class InstanceVarProvider(BaseProvider):
727 """This class loads config values from the session instance vars."""
728
729 def __init__(self, instance_var, session):
730 """Initialize InstanceVarProvider.
731
732 :type instance_var: str
733 :param instance_var: The instance variable to load from the session.
734
735 :type session: :class:`botocore.session.Session`
736 :param session: The botocore session to get the loaded configuration
737 file variables from.
738 """
739 self._instance_var = instance_var
740 self._session = session
741
742 def __deepcopy__(self, memo):
743 return InstanceVarProvider(
744 copy.deepcopy(self._instance_var, memo), self._session
745 )
746
747 def provide(self):
748 """Provide a config value from the session instance vars."""
749 instance_vars = self._session.instance_variables()
750 value = instance_vars.get(self._instance_var)
751 return value
752
753 def __repr__(self):
754 return f'InstanceVarProvider(instance_var={self._instance_var}, session={self._session})'
755
756
757class ScopedConfigProvider(BaseProvider):
758 def __init__(self, config_var_name, session):
759 """Initialize ScopedConfigProvider.
760
761 :type config_var_name: str or tuple
762 :param config_var_name: The name of the config variable to load from
763 the configuration file. If the value is a tuple, it must only
764 consist of two items, where the first item represents the section
765 and the second item represents the config var name in the section.
766
767 :type session: :class:`botocore.session.Session`
768 :param session: The botocore session to get the loaded configuration
769 file variables from.
770 """
771 self._config_var_name = config_var_name
772 self._session = session
773
774 def __deepcopy__(self, memo):
775 return ScopedConfigProvider(
776 copy.deepcopy(self._config_var_name, memo), self._session
777 )
778
779 def provide(self):
780 """Provide a value from a config file property."""
781 scoped_config = self._session.get_scoped_config()
782 if isinstance(self._config_var_name, tuple):
783 section_config = scoped_config.get(self._config_var_name[0])
784 if not isinstance(section_config, dict):
785 return None
786 return section_config.get(self._config_var_name[1])
787 return scoped_config.get(self._config_var_name)
788
789 def __repr__(self):
790 return f'ScopedConfigProvider(config_var_name={self._config_var_name}, session={self._session})'
791
792
793class EnvironmentProvider(BaseProvider):
794 """This class loads config values from environment variables."""
795
796 def __init__(self, name, env):
797 """Initialize with the keys in the dictionary to check.
798
799 :type name: str
800 :param name: The key with that name will be loaded and returned.
801
802 :type env: dict
803 :param env: Environment variables dictionary to get variables from.
804 """
805 self._name = name
806 self._env = env
807
808 def __deepcopy__(self, memo):
809 return EnvironmentProvider(
810 copy.deepcopy(self._name, memo), copy.deepcopy(self._env, memo)
811 )
812
813 def provide(self):
814 """Provide a config value from a source dictionary."""
815 if self._name in self._env:
816 return self._env[self._name]
817 return None
818
819 def __repr__(self):
820 return f'EnvironmentProvider(name={self._name}, env={self._env})'
821
822
823class SectionConfigProvider(BaseProvider):
824 """Provides a dictionary from a section in the scoped config
825
826 This is useful for retrieving scoped config variables (i.e. s3) that have
827 their own set of config variables and resolving logic.
828 """
829
830 def __init__(self, section_name, session, override_providers=None):
831 self._section_name = section_name
832 self._session = session
833 self._scoped_config_provider = ScopedConfigProvider(
834 self._section_name, self._session
835 )
836 self._override_providers = override_providers
837 if self._override_providers is None:
838 self._override_providers = {}
839
840 def __deepcopy__(self, memo):
841 return SectionConfigProvider(
842 copy.deepcopy(self._section_name, memo),
843 self._session,
844 copy.deepcopy(self._override_providers, memo),
845 )
846
847 def provide(self):
848 section_config = self._scoped_config_provider.provide()
849 if section_config and not isinstance(section_config, dict):
850 logger.debug(
851 "The %s config key is not a dictionary type, "
852 "ignoring its value of: %s",
853 self._section_name,
854 section_config,
855 )
856 return None
857 for section_config_var, provider in self._override_providers.items():
858 provider_val = provider.provide()
859 if provider_val is not None:
860 if section_config is None:
861 section_config = {}
862 section_config[section_config_var] = provider_val
863 return section_config
864
865 def set_default_provider(self, key, default_provider):
866 provider = self._override_providers.get(key)
867 if isinstance(provider, ChainProvider):
868 provider.set_default_provider(default_provider)
869 return
870 elif isinstance(provider, BaseProvider):
871 default_provider = ChainProvider(
872 providers=[provider, default_provider]
873 )
874 self._override_providers[key] = default_provider
875
876 def __repr__(self):
877 return (
878 f'SectionConfigProvider(section_name={self._section_name}, '
879 f'session={self._session}, '
880 f'override_providers={self._override_providers})'
881 )
882
883
884class ConstantProvider(BaseProvider):
885 """This provider provides a constant value."""
886
887 def __init__(self, value):
888 self._value = value
889
890 def __deepcopy__(self, memo):
891 return ConstantProvider(copy.deepcopy(self._value, memo))
892
893 def provide(self):
894 """Provide the constant value given during initialization."""
895 return self._value
896
897 def __repr__(self):
898 return f'ConstantProvider(value={self._value})'
899
900
901class ConfiguredEndpointProvider(BaseProvider):
902 """Lookup an endpoint URL from environment variable or shared config file.
903
904 NOTE: This class is considered private and is subject to abrupt breaking
905 changes or removal without prior announcement. Please do not use it
906 directly.
907 """
908
909 _ENDPOINT_URL_LOOKUP_ORDER = [
910 'environment_service',
911 'environment_global',
912 'config_service',
913 'config_global',
914 ]
915
916 def __init__(
917 self,
918 full_config,
919 scoped_config,
920 client_name,
921 environ=None,
922 ):
923 """Initialize a ConfiguredEndpointProviderChain.
924
925 :type full_config: dict
926 :param full_config: This is the dict representing the full
927 configuration file.
928
929 :type scoped_config: dict
930 :param scoped_config: This is the dict representing the configuration
931 for the current profile for the session.
932
933 :type client_name: str
934 :param client_name: The name used to instantiate a client using
935 botocore.session.Session.create_client.
936
937 :type environ: dict
938 :param environ: A mapping to use for environment variables. If this
939 is not provided it will default to use os.environ.
940 """
941 self._full_config = full_config
942 self._scoped_config = scoped_config
943 self._client_name = client_name
944 self._transformed_service_id = self._get_snake_case_service_id(
945 self._client_name
946 )
947 if environ is None:
948 environ = os.environ
949 self._environ = environ
950
951 def provide(self):
952 """Lookup the configured endpoint URL.
953
954 The order is:
955
956 1. The value provided by a service-specific environment variable.
957 2. The value provided by the global endpoint environment variable
958 (AWS_ENDPOINT_URL).
959 3. The value provided by a service-specific parameter from a services
960 definition section in the shared configuration file.
961 4. The value provided by the global parameter from a services
962 definition section in the shared configuration file.
963 """
964 for location in self._ENDPOINT_URL_LOOKUP_ORDER:
965 logger.debug(
966 'Looking for endpoint for %s via: %s',
967 self._client_name,
968 location,
969 )
970
971 endpoint_url = getattr(self, f'_get_endpoint_url_{location}')()
972
973 if endpoint_url:
974 logger.info(
975 'Found endpoint for %s via: %s.',
976 self._client_name,
977 location,
978 )
979 return endpoint_url
980
981 logger.debug('No configured endpoint found.')
982 return None
983
984 def _get_snake_case_service_id(self, client_name):
985 # Get the service ID without loading the service data file, accounting
986 # for any aliases and standardizing the names with hyphens.
987 client_name = utils.SERVICE_NAME_ALIASES.get(client_name, client_name)
988 hyphenized_service_id = (
989 utils.CLIENT_NAME_TO_HYPHENIZED_SERVICE_ID_OVERRIDES.get(
990 client_name, client_name
991 )
992 )
993 return hyphenized_service_id.replace('-', '_')
994
995 def _get_service_env_var_name(self):
996 transformed_service_id_env = self._transformed_service_id.upper()
997 return f'AWS_ENDPOINT_URL_{transformed_service_id_env}'
998
999 def _get_services_config(self):
1000 if 'services' not in self._scoped_config:
1001 return {}
1002
1003 section_name = self._scoped_config['services']
1004 services_section = self._full_config.get('services', {}).get(
1005 section_name
1006 )
1007
1008 if not services_section:
1009 error_msg = (
1010 f'The profile is configured to use the services '
1011 f'section but the "{section_name}" services '
1012 f'configuration does not exist.'
1013 )
1014 raise InvalidConfigError(error_msg=error_msg)
1015
1016 return services_section
1017
1018 def _get_endpoint_url_config_service(self):
1019 snakecase_service_id = self._transformed_service_id.lower()
1020 return (
1021 self._get_services_config()
1022 .get(snakecase_service_id, {})
1023 .get('endpoint_url')
1024 )
1025
1026 def _get_endpoint_url_config_global(self):
1027 return self._scoped_config.get('endpoint_url')
1028
1029 def _get_endpoint_url_environment_service(self):
1030 return EnvironmentProvider(
1031 name=self._get_service_env_var_name(), env=self._environ
1032 ).provide()
1033
1034 def _get_endpoint_url_environment_global(self):
1035 return EnvironmentProvider(
1036 name='AWS_ENDPOINT_URL', env=self._environ
1037 ).provide()