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 'request_checksum_calculation': (
172 'request_checksum_calculation',
173 'AWS_REQUEST_CHECKSUM_CALCULATION',
174 "when_supported",
175 None,
176 ),
177 'response_checksum_validation': (
178 'response_checksum_validation',
179 'AWS_RESPONSE_CHECKSUM_VALIDATION',
180 "when_supported",
181 None,
182 ),
183 'account_id_endpoint_mode': (
184 'account_id_endpoint_mode',
185 'AWS_ACCOUNT_ID_ENDPOINT_MODE',
186 'preferred',
187 None,
188 ),
189 'disable_host_prefix_injection': (
190 'disable_host_prefix_injection',
191 'AWS_DISABLE_HOST_PREFIX_INJECTION',
192 None,
193 utils.ensure_boolean,
194 ),
195 'auth_scheme_preference': (
196 'auth_scheme_preference',
197 'AWS_AUTH_SCHEME_PREFERENCE',
198 None,
199 None,
200 ),
201}
202
203# Evaluate AWS_STS_REGIONAL_ENDPOINTS settings
204try:
205 # This is not a public interface and is subject to abrupt breaking changes.
206 # Any usage is not advised or supported in external code bases.
207 from botocore.customizations.sts import (
208 sts_default_setting as _sts_default_setting,
209 )
210except ImportError:
211 _sts_default_setting = 'legacy'
212
213_STS_DEFAULT_SETTINGS = {
214 'sts_regional_endpoints': (
215 'sts_regional_endpoints',
216 'AWS_STS_REGIONAL_ENDPOINTS',
217 _sts_default_setting,
218 None,
219 ),
220}
221BOTOCORE_DEFAUT_SESSION_VARIABLES.update(_STS_DEFAULT_SETTINGS)
222
223
224# A mapping for the s3 specific configuration vars. These are the configuration
225# vars that typically go in the s3 section of the config file. This mapping
226# follows the same schema as the previous session variable mapping.
227DEFAULT_S3_CONFIG_VARS = {
228 'addressing_style': (('s3', 'addressing_style'), None, None, None),
229 'use_accelerate_endpoint': (
230 ('s3', 'use_accelerate_endpoint'),
231 None,
232 None,
233 utils.ensure_boolean,
234 ),
235 'use_dualstack_endpoint': (
236 ('s3', 'use_dualstack_endpoint'),
237 None,
238 None,
239 utils.ensure_boolean,
240 ),
241 'payload_signing_enabled': (
242 ('s3', 'payload_signing_enabled'),
243 None,
244 None,
245 utils.ensure_boolean,
246 ),
247 'use_arn_region': (
248 ['s3_use_arn_region', ('s3', 'use_arn_region')],
249 'AWS_S3_USE_ARN_REGION',
250 None,
251 utils.ensure_boolean,
252 ),
253 'us_east_1_regional_endpoint': (
254 [
255 's3_us_east_1_regional_endpoint',
256 ('s3', 'us_east_1_regional_endpoint'),
257 ],
258 'AWS_S3_US_EAST_1_REGIONAL_ENDPOINT',
259 None,
260 None,
261 ),
262 's3_disable_multiregion_access_points': (
263 ('s3', 's3_disable_multiregion_access_points'),
264 'AWS_S3_DISABLE_MULTIREGION_ACCESS_POINTS',
265 None,
266 utils.ensure_boolean,
267 ),
268}
269# A mapping for the proxy specific configuration vars. These are
270# used to configure how botocore interacts with proxy setups while
271# sending requests.
272DEFAULT_PROXIES_CONFIG_VARS = {
273 'proxy_ca_bundle': ('proxy_ca_bundle', None, None, None),
274 'proxy_client_cert': ('proxy_client_cert', None, None, None),
275 'proxy_use_forwarding_for_https': (
276 'proxy_use_forwarding_for_https',
277 None,
278 None,
279 utils.normalize_boolean,
280 ),
281}
282
283
284def create_botocore_default_config_mapping(session):
285 chain_builder = ConfigChainFactory(session=session)
286 config_mapping = _create_config_chain_mapping(
287 chain_builder, BOTOCORE_DEFAUT_SESSION_VARIABLES
288 )
289 config_mapping['s3'] = SectionConfigProvider(
290 's3',
291 session,
292 _create_config_chain_mapping(chain_builder, DEFAULT_S3_CONFIG_VARS),
293 )
294 config_mapping['proxies_config'] = SectionConfigProvider(
295 'proxies_config',
296 session,
297 _create_config_chain_mapping(
298 chain_builder, DEFAULT_PROXIES_CONFIG_VARS
299 ),
300 )
301 return config_mapping
302
303
304def _create_config_chain_mapping(chain_builder, config_variables):
305 mapping = {}
306 for logical_name, config in config_variables.items():
307 mapping[logical_name] = chain_builder.create_config_chain(
308 instance_name=logical_name,
309 env_var_names=config[1],
310 config_property_names=config[0],
311 default=config[2],
312 conversion_func=config[3],
313 )
314 return mapping
315
316
317class DefaultConfigResolver:
318 def __init__(self, default_config_data):
319 self._base_default_config = default_config_data['base']
320 self._modes = default_config_data['modes']
321 self._resolved_default_configurations = {}
322
323 def _resolve_default_values_by_mode(self, mode):
324 default_config = self._base_default_config.copy()
325 modifications = self._modes.get(mode)
326
327 for config_var in modifications:
328 default_value = default_config[config_var]
329 modification_dict = modifications[config_var]
330 modification = list(modification_dict.keys())[0]
331 modification_value = modification_dict[modification]
332 if modification == 'multiply':
333 default_value *= modification_value
334 elif modification == 'add':
335 default_value += modification_value
336 elif modification == 'override':
337 default_value = modification_value
338 default_config[config_var] = default_value
339 return default_config
340
341 def get_default_modes(self):
342 default_modes = ['legacy', 'auto']
343 default_modes.extend(self._modes.keys())
344 return default_modes
345
346 def get_default_config_values(self, mode):
347 if mode not in self._resolved_default_configurations:
348 defaults = self._resolve_default_values_by_mode(mode)
349 self._resolved_default_configurations[mode] = defaults
350 return self._resolved_default_configurations[mode]
351
352
353class ConfigChainFactory:
354 """Factory class to create our most common configuration chain case.
355
356 This is a convenience class to construct configuration chains that follow
357 our most common pattern. This is to prevent ordering them incorrectly,
358 and to make the config chain construction more readable.
359 """
360
361 def __init__(self, session, environ=None):
362 """Initialize a ConfigChainFactory.
363
364 :type session: :class:`botocore.session.Session`
365 :param session: This is the session that should be used to look up
366 values from the config file.
367
368 :type environ: dict
369 :param environ: A mapping to use for environment variables. If this
370 is not provided it will default to use os.environ.
371 """
372 self._session = session
373 if environ is None:
374 environ = os.environ
375 self._environ = environ
376
377 def create_config_chain(
378 self,
379 instance_name=None,
380 env_var_names=None,
381 config_property_names=None,
382 default=None,
383 conversion_func=None,
384 ):
385 """Build a config chain following the standard botocore pattern.
386
387 In botocore most of our config chains follow the the precendence:
388 session_instance_variables, environment, config_file, default_value.
389
390 This is a convenience function for creating a chain that follow
391 that precendence.
392
393 :type instance_name: str
394 :param instance_name: This indicates what session instance variable
395 corresponds to this config value. If it is None it will not be
396 added to the chain.
397
398 :type env_var_names: str or list of str or None
399 :param env_var_names: One or more environment variable names to
400 search for this value. They are searched in order. If it is None
401 it will not be added to the chain.
402
403 :type config_property_names: str/tuple or list of str/tuple or None
404 :param config_property_names: One of more strings or tuples
405 representing the name of the key in the config file for this
406 config option. They are searched in order. If it is None it will
407 not be added to the chain.
408
409 :type default: Any
410 :param default: Any constant value to be returned.
411
412 :type conversion_func: None or callable
413 :param conversion_func: If this value is None then it has no effect on
414 the return type. Otherwise, it is treated as a function that will
415 conversion_func our provided type.
416
417 :rvalue: ConfigChain
418 :returns: A ConfigChain that resolves in the order env_var_names ->
419 config_property_name -> default. Any values that were none are
420 omitted form the chain.
421 """
422 providers = []
423 if instance_name is not None:
424 providers.append(
425 InstanceVarProvider(
426 instance_var=instance_name, session=self._session
427 )
428 )
429 if env_var_names is not None:
430 providers.extend(self._get_env_providers(env_var_names))
431 if config_property_names is not None:
432 providers.extend(
433 self._get_scoped_config_providers(config_property_names)
434 )
435 if default is not None:
436 providers.append(ConstantProvider(value=default))
437
438 return ChainProvider(
439 providers=providers,
440 conversion_func=conversion_func,
441 )
442
443 def _get_env_providers(self, env_var_names):
444 env_var_providers = []
445 if not isinstance(env_var_names, list):
446 env_var_names = [env_var_names]
447 for env_var_name in env_var_names:
448 env_var_providers.append(
449 EnvironmentProvider(name=env_var_name, env=self._environ)
450 )
451 return env_var_providers
452
453 def _get_scoped_config_providers(self, config_property_names):
454 scoped_config_providers = []
455 if not isinstance(config_property_names, list):
456 config_property_names = [config_property_names]
457 for config_property_name in config_property_names:
458 scoped_config_providers.append(
459 ScopedConfigProvider(
460 config_var_name=config_property_name,
461 session=self._session,
462 )
463 )
464 return scoped_config_providers
465
466
467class ConfigValueStore:
468 """The ConfigValueStore object stores configuration values."""
469
470 def __init__(self, mapping=None):
471 """Initialize a ConfigValueStore.
472
473 :type mapping: dict
474 :param mapping: The mapping parameter is a map of string to a subclass
475 of BaseProvider. When a config variable is asked for via the
476 get_config_variable method, the corresponding provider will be
477 invoked to load the value.
478 """
479 self._overrides = {}
480 self._mapping = {}
481 if mapping is not None:
482 for logical_name, provider in mapping.items():
483 self.set_config_provider(logical_name, provider)
484
485 def __deepcopy__(self, memo):
486 config_store = ConfigValueStore(copy.deepcopy(self._mapping, memo))
487 for logical_name, override_value in self._overrides.items():
488 config_store.set_config_variable(logical_name, override_value)
489
490 return config_store
491
492 def __copy__(self):
493 config_store = ConfigValueStore(copy.copy(self._mapping))
494 for logical_name, override_value in self._overrides.items():
495 config_store.set_config_variable(logical_name, override_value)
496
497 return config_store
498
499 def get_config_variable(self, logical_name):
500 """
501 Retrieve the value associated with the specified logical_name
502 from the corresponding provider. If no value is found None will
503 be returned.
504
505 :type logical_name: str
506 :param logical_name: The logical name of the session variable
507 you want to retrieve. This name will be mapped to the
508 appropriate environment variable name for this session as
509 well as the appropriate config file entry.
510
511 :returns: value of variable or None if not defined.
512 """
513 if logical_name in self._overrides:
514 return self._overrides[logical_name]
515 if logical_name not in self._mapping:
516 return None
517 provider = self._mapping[logical_name]
518 return provider.provide()
519
520 def get_config_provider(self, logical_name):
521 """
522 Retrieve the provider associated with the specified logical_name.
523 If no provider is found None will be returned.
524
525 :type logical_name: str
526 :param logical_name: The logical name of the session variable
527 you want to retrieve. This name will be mapped to the
528 appropriate environment variable name for this session as
529 well as the appropriate config file entry.
530
531 :returns: configuration provider or None if not defined.
532 """
533 if (
534 logical_name in self._overrides
535 or logical_name not in self._mapping
536 ):
537 return None
538 provider = self._mapping[logical_name]
539 return provider
540
541 def set_config_variable(self, logical_name, value):
542 """Set a configuration variable to a specific value.
543
544 By using this method, you can override the normal lookup
545 process used in ``get_config_variable`` by explicitly setting
546 a value. Subsequent calls to ``get_config_variable`` will
547 use the ``value``. This gives you per-session specific
548 configuration values.
549
550 ::
551 >>> # Assume logical name 'foo' maps to env var 'FOO'
552 >>> os.environ['FOO'] = 'myvalue'
553 >>> s.get_config_variable('foo')
554 'myvalue'
555 >>> s.set_config_variable('foo', 'othervalue')
556 >>> s.get_config_variable('foo')
557 'othervalue'
558
559 :type logical_name: str
560 :param logical_name: The logical name of the session variable
561 you want to set. These are the keys in ``SESSION_VARIABLES``.
562
563 :param value: The value to associate with the config variable.
564 """
565 self._overrides[logical_name] = value
566
567 def clear_config_variable(self, logical_name):
568 """Remove an override config variable from the session.
569
570 :type logical_name: str
571 :param logical_name: The name of the parameter to clear the override
572 value from.
573 """
574 self._overrides.pop(logical_name, None)
575
576 def set_config_provider(self, logical_name, provider):
577 """Set the provider for a config value.
578
579 This provides control over how a particular configuration value is
580 loaded. This replaces the provider for ``logical_name`` with the new
581 ``provider``.
582
583 :type logical_name: str
584 :param logical_name: The name of the config value to change the config
585 provider for.
586
587 :type provider: :class:`botocore.configprovider.BaseProvider`
588 :param provider: The new provider that should be responsible for
589 providing a value for the config named ``logical_name``.
590 """
591 self._mapping[logical_name] = provider
592
593
594class SmartDefaultsConfigStoreFactory:
595 def __init__(self, default_config_resolver, imds_region_provider):
596 self._default_config_resolver = default_config_resolver
597 self._imds_region_provider = imds_region_provider
598 # Initializing _instance_metadata_region as None so we
599 # can fetch region in a lazy fashion only when needed.
600 self._instance_metadata_region = None
601
602 def merge_smart_defaults(self, config_store, mode, region_name):
603 if mode == 'auto':
604 mode = self.resolve_auto_mode(region_name)
605 default_configs = (
606 self._default_config_resolver.get_default_config_values(mode)
607 )
608 for config_var in default_configs:
609 config_value = default_configs[config_var]
610 method = getattr(self, f'_set_{config_var}', None)
611 if method:
612 method(config_store, config_value)
613
614 def resolve_auto_mode(self, region_name):
615 current_region = None
616 if os.environ.get('AWS_EXECUTION_ENV'):
617 default_region = os.environ.get('AWS_DEFAULT_REGION')
618 current_region = os.environ.get('AWS_REGION', default_region)
619 if not current_region:
620 if self._instance_metadata_region:
621 current_region = self._instance_metadata_region
622 else:
623 try:
624 current_region = self._imds_region_provider.provide()
625 self._instance_metadata_region = current_region
626 except Exception:
627 pass
628
629 if current_region:
630 if region_name == current_region:
631 return 'in-region'
632 else:
633 return 'cross-region'
634 return 'standard'
635
636 def _update_provider(self, config_store, variable, value):
637 original_provider = config_store.get_config_provider(variable)
638 default_provider = ConstantProvider(value)
639 if isinstance(original_provider, ChainProvider):
640 chain_provider_copy = copy.deepcopy(original_provider)
641 chain_provider_copy.set_default_provider(default_provider)
642 default_provider = chain_provider_copy
643 elif isinstance(original_provider, BaseProvider):
644 default_provider = ChainProvider(
645 providers=[original_provider, default_provider]
646 )
647 config_store.set_config_provider(variable, default_provider)
648
649 def _update_section_provider(
650 self, config_store, section_name, variable, value
651 ):
652 section_provider_copy = copy.deepcopy(
653 config_store.get_config_provider(section_name)
654 )
655 section_provider_copy.set_default_provider(
656 variable, ConstantProvider(value)
657 )
658 config_store.set_config_provider(section_name, section_provider_copy)
659
660 def _set_retryMode(self, config_store, value):
661 self._update_provider(config_store, 'retry_mode', value)
662
663 def _set_stsRegionalEndpoints(self, config_store, value):
664 self._update_provider(config_store, 'sts_regional_endpoints', value)
665
666 def _set_s3UsEast1RegionalEndpoints(self, config_store, value):
667 self._update_section_provider(
668 config_store, 's3', 'us_east_1_regional_endpoint', value
669 )
670
671 def _set_connectTimeoutInMillis(self, config_store, value):
672 self._update_provider(config_store, 'connect_timeout', value / 1000)
673
674
675class BaseProvider:
676 """Base class for configuration value providers.
677
678 A configuration provider has some method of providing a configuration
679 value.
680 """
681
682 def provide(self):
683 """Provide a config value."""
684 raise NotImplementedError('provide')
685
686
687class ChainProvider(BaseProvider):
688 """This provider wraps one or more other providers.
689
690 Each provider in the chain is called, the first one returning a non-None
691 value is then returned.
692 """
693
694 def __init__(self, providers=None, conversion_func=None):
695 """Initalize a ChainProvider.
696
697 :type providers: list
698 :param providers: The initial list of providers to check for values
699 when invoked.
700
701 :type conversion_func: None or callable
702 :param conversion_func: If this value is None then it has no affect on
703 the return type. Otherwise, it is treated as a function that will
704 transform provided value.
705 """
706 if providers is None:
707 providers = []
708 self._providers = providers
709 self._conversion_func = conversion_func
710
711 def __deepcopy__(self, memo):
712 return ChainProvider(
713 copy.deepcopy(self._providers, memo), self._conversion_func
714 )
715
716 def provide(self):
717 """Provide the value from the first provider to return non-None.
718
719 Each provider in the chain has its provide method called. The first
720 one in the chain to return a non-None value is the returned from the
721 ChainProvider. When no non-None value is found, None is returned.
722 """
723 for provider in self._providers:
724 value = provider.provide()
725 if value is not None:
726 return self._convert_type(value)
727 return None
728
729 def set_default_provider(self, default_provider):
730 if self._providers and isinstance(
731 self._providers[-1], ConstantProvider
732 ):
733 self._providers[-1] = default_provider
734 else:
735 self._providers.append(default_provider)
736
737 num_of_constants = sum(
738 isinstance(provider, ConstantProvider)
739 for provider in self._providers
740 )
741 if num_of_constants > 1:
742 logger.info(
743 'ChainProvider object contains multiple '
744 'instances of ConstantProvider objects'
745 )
746
747 def _convert_type(self, value):
748 if self._conversion_func is not None:
749 return self._conversion_func(value)
750 return value
751
752 def __repr__(self):
753 return '[{}]'.format(', '.join([str(p) for p in self._providers]))
754
755
756class InstanceVarProvider(BaseProvider):
757 """This class loads config values from the session instance vars."""
758
759 def __init__(self, instance_var, session):
760 """Initialize InstanceVarProvider.
761
762 :type instance_var: str
763 :param instance_var: The instance variable to load from the session.
764
765 :type session: :class:`botocore.session.Session`
766 :param session: The botocore session to get the loaded configuration
767 file variables from.
768 """
769 self._instance_var = instance_var
770 self._session = session
771
772 def __deepcopy__(self, memo):
773 return InstanceVarProvider(
774 copy.deepcopy(self._instance_var, memo), self._session
775 )
776
777 def provide(self):
778 """Provide a config value from the session instance vars."""
779 instance_vars = self._session.instance_variables()
780 value = instance_vars.get(self._instance_var)
781 return value
782
783 def __repr__(self):
784 return f'InstanceVarProvider(instance_var={self._instance_var}, session={self._session})'
785
786
787class ScopedConfigProvider(BaseProvider):
788 def __init__(self, config_var_name, session):
789 """Initialize ScopedConfigProvider.
790
791 :type config_var_name: str or tuple
792 :param config_var_name: The name of the config variable to load from
793 the configuration file. If the value is a tuple, it must only
794 consist of two items, where the first item represents the section
795 and the second item represents the config var name in the section.
796
797 :type session: :class:`botocore.session.Session`
798 :param session: The botocore session to get the loaded configuration
799 file variables from.
800 """
801 self._config_var_name = config_var_name
802 self._session = session
803
804 def __deepcopy__(self, memo):
805 return ScopedConfigProvider(
806 copy.deepcopy(self._config_var_name, memo), self._session
807 )
808
809 def provide(self):
810 """Provide a value from a config file property."""
811 scoped_config = self._session.get_scoped_config()
812 if isinstance(self._config_var_name, tuple):
813 section_config = scoped_config.get(self._config_var_name[0])
814 if not isinstance(section_config, dict):
815 return None
816 return section_config.get(self._config_var_name[1])
817 return scoped_config.get(self._config_var_name)
818
819 def __repr__(self):
820 return f'ScopedConfigProvider(config_var_name={self._config_var_name}, session={self._session})'
821
822
823class EnvironmentProvider(BaseProvider):
824 """This class loads config values from environment variables."""
825
826 def __init__(self, name, env):
827 """Initialize with the keys in the dictionary to check.
828
829 :type name: str
830 :param name: The key with that name will be loaded and returned.
831
832 :type env: dict
833 :param env: Environment variables dictionary to get variables from.
834 """
835 self._name = name
836 self._env = env
837
838 def __deepcopy__(self, memo):
839 return EnvironmentProvider(
840 copy.deepcopy(self._name, memo), copy.deepcopy(self._env, memo)
841 )
842
843 def provide(self):
844 """Provide a config value from a source dictionary."""
845 if self._name in self._env:
846 return self._env[self._name]
847 return None
848
849 def __repr__(self):
850 return f'EnvironmentProvider(name={self._name}, env={self._env})'
851
852
853class SectionConfigProvider(BaseProvider):
854 """Provides a dictionary from a section in the scoped config
855
856 This is useful for retrieving scoped config variables (i.e. s3) that have
857 their own set of config variables and resolving logic.
858 """
859
860 def __init__(self, section_name, session, override_providers=None):
861 self._section_name = section_name
862 self._session = session
863 self._scoped_config_provider = ScopedConfigProvider(
864 self._section_name, self._session
865 )
866 self._override_providers = override_providers
867 if self._override_providers is None:
868 self._override_providers = {}
869
870 def __deepcopy__(self, memo):
871 return SectionConfigProvider(
872 copy.deepcopy(self._section_name, memo),
873 self._session,
874 copy.deepcopy(self._override_providers, memo),
875 )
876
877 def provide(self):
878 section_config = self._scoped_config_provider.provide()
879 if section_config and not isinstance(section_config, dict):
880 logger.debug(
881 "The %s config key is not a dictionary type, "
882 "ignoring its value of: %s",
883 self._section_name,
884 section_config,
885 )
886 return None
887 for section_config_var, provider in self._override_providers.items():
888 provider_val = provider.provide()
889 if provider_val is not None:
890 if section_config is None:
891 section_config = {}
892 section_config[section_config_var] = provider_val
893 return section_config
894
895 def set_default_provider(self, key, default_provider):
896 provider = self._override_providers.get(key)
897 if isinstance(provider, ChainProvider):
898 provider.set_default_provider(default_provider)
899 return
900 elif isinstance(provider, BaseProvider):
901 default_provider = ChainProvider(
902 providers=[provider, default_provider]
903 )
904 self._override_providers[key] = default_provider
905
906 def __repr__(self):
907 return (
908 f'SectionConfigProvider(section_name={self._section_name}, '
909 f'session={self._session}, '
910 f'override_providers={self._override_providers})'
911 )
912
913
914class ConstantProvider(BaseProvider):
915 """This provider provides a constant value."""
916
917 def __init__(self, value):
918 self._value = value
919
920 def __deepcopy__(self, memo):
921 return ConstantProvider(copy.deepcopy(self._value, memo))
922
923 def provide(self):
924 """Provide the constant value given during initialization."""
925 return self._value
926
927 def __repr__(self):
928 return f'ConstantProvider(value={self._value})'
929
930
931class ConfiguredEndpointProvider(BaseProvider):
932 """Lookup an endpoint URL from environment variable or shared config file.
933
934 NOTE: This class is considered private and is subject to abrupt breaking
935 changes or removal without prior announcement. Please do not use it
936 directly.
937 """
938
939 _ENDPOINT_URL_LOOKUP_ORDER = [
940 'environment_service',
941 'environment_global',
942 'config_service',
943 'config_global',
944 ]
945
946 def __init__(
947 self,
948 full_config,
949 scoped_config,
950 client_name,
951 environ=None,
952 ):
953 """Initialize a ConfiguredEndpointProviderChain.
954
955 :type full_config: dict
956 :param full_config: This is the dict representing the full
957 configuration file.
958
959 :type scoped_config: dict
960 :param scoped_config: This is the dict representing the configuration
961 for the current profile for the session.
962
963 :type client_name: str
964 :param client_name: The name used to instantiate a client using
965 botocore.session.Session.create_client.
966
967 :type environ: dict
968 :param environ: A mapping to use for environment variables. If this
969 is not provided it will default to use os.environ.
970 """
971 self._full_config = full_config
972 self._scoped_config = scoped_config
973 self._client_name = client_name
974 self._transformed_service_id = self._get_snake_case_service_id(
975 self._client_name
976 )
977 if environ is None:
978 environ = os.environ
979 self._environ = environ
980
981 def provide(self):
982 """Lookup the configured endpoint URL.
983
984 The order is:
985
986 1. The value provided by a service-specific environment variable.
987 2. The value provided by the global endpoint environment variable
988 (AWS_ENDPOINT_URL).
989 3. The value provided by a service-specific parameter from a services
990 definition section in the shared configuration file.
991 4. The value provided by the global parameter from a services
992 definition section in the shared configuration file.
993 """
994 for location in self._ENDPOINT_URL_LOOKUP_ORDER:
995 logger.debug(
996 'Looking for endpoint for %s via: %s',
997 self._client_name,
998 location,
999 )
1000
1001 endpoint_url = getattr(self, f'_get_endpoint_url_{location}')()
1002
1003 if endpoint_url:
1004 logger.info(
1005 'Found endpoint for %s via: %s.',
1006 self._client_name,
1007 location,
1008 )
1009 return endpoint_url
1010
1011 logger.debug('No configured endpoint found.')
1012 return None
1013
1014 def _get_snake_case_service_id(self, client_name):
1015 # Get the service ID without loading the service data file, accounting
1016 # for any aliases and standardizing the names with hyphens.
1017 client_name = utils.SERVICE_NAME_ALIASES.get(client_name, client_name)
1018 hyphenized_service_id = (
1019 utils.CLIENT_NAME_TO_HYPHENIZED_SERVICE_ID_OVERRIDES.get(
1020 client_name, client_name
1021 )
1022 )
1023 return hyphenized_service_id.replace('-', '_')
1024
1025 def _get_service_env_var_name(self):
1026 transformed_service_id_env = self._transformed_service_id.upper()
1027 return f'AWS_ENDPOINT_URL_{transformed_service_id_env}'
1028
1029 def _get_services_config(self):
1030 if 'services' not in self._scoped_config:
1031 return {}
1032
1033 section_name = self._scoped_config['services']
1034 services_section = self._full_config.get('services', {}).get(
1035 section_name
1036 )
1037
1038 if not services_section:
1039 error_msg = (
1040 f'The profile is configured to use the services '
1041 f'section but the "{section_name}" services '
1042 f'configuration does not exist.'
1043 )
1044 raise InvalidConfigError(error_msg=error_msg)
1045
1046 return services_section
1047
1048 def _get_endpoint_url_config_service(self):
1049 snakecase_service_id = self._transformed_service_id.lower()
1050 return (
1051 self._get_services_config()
1052 .get(snakecase_service_id, {})
1053 .get('endpoint_url')
1054 )
1055
1056 def _get_endpoint_url_config_global(self):
1057 return self._scoped_config.get('endpoint_url')
1058
1059 def _get_endpoint_url_environment_service(self):
1060 return EnvironmentProvider(
1061 name=self._get_service_env_var_name(), env=self._environ
1062 ).provide()
1063
1064 def _get_endpoint_url_environment_global(self):
1065 return EnvironmentProvider(
1066 name='AWS_ENDPOINT_URL', env=self._environ
1067 ).provide()