From 336036995c985754afc0ce2579ceef6c98dc3f8b Mon Sep 17 00:00:00 2001 From: Kenneth Daily Date: Thu, 26 Feb 2026 12:11:40 -0800 Subject: [PATCH 1/5] Add ability to set properties in subsections This change adds new parameters to the `aws configure set`` command to specify a sub-section for setting a property. These parameters are analogous to the existing `--profile` parameter. A parameter will be added for each sub-section type and take a value of the subsection name. Following is the generic pattern for the `aws configure set` command: ``` aws configure set -- \ ``` For example, the following command should set the property `sso_region` to the value `us-west-2` in the `sso-session` sub-section named `my-sso-session`: ``` aws configure set --sso-session my-sso-session \ sso_region us-west-2 ``` Following is an example setting a nested property in a sub-section: ``` aws configure set \ -- \ . value ``` The only sub-section types allowed are `services` and `sso-session`. --- awscli/customizations/configure/__init__.py | 13 +++- awscli/customizations/configure/set.py | 74 +++++++++++++++++- tests/functional/configure/test_configure.py | 75 +++++++++++++++++++ .../unit/customizations/configure/test_set.py | 25 +++++++ 4 files changed, 185 insertions(+), 2 deletions(-) diff --git a/awscli/customizations/configure/__init__.py b/awscli/customizations/configure/__init__.py index 7782d7e4d43f..990d53afb4e7 100644 --- a/awscli/customizations/configure/__init__.py +++ b/awscli/customizations/configure/__init__.py @@ -10,12 +10,23 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -import string from awscli.compat import shlex NOT_SET = '' PREDEFINED_SECTION_NAMES = 'plugins' +# A map between the command line parameter name and the Python object name +# For allowed sub-section types +SUBSECTION_TYPE_ALLOWLIST = { + 'sso-session': { + "param_name" :'sso_session', + "full_config_name": "sso_sessions" + }, + 'services': { + 'param_name': 'services', + "full_config_name": "services" + }, +} _WHITESPACE = ' \t' diff --git a/awscli/customizations/configure/set.py b/awscli/customizations/configure/set.py index 7f45ee8fd318..4400693aeeda 100644 --- a/awscli/customizations/configure/set.py +++ b/awscli/customizations/configure/set.py @@ -11,11 +11,19 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. import os +import re from awscli.customizations.commands import BasicCommand from awscli.customizations.configure.writer import ConfigFileWriter +from awscli.customizations.exceptions import ParamValidationError +from awscli.customizations.utils import validate_mutually_exclusive -from . import PREDEFINED_SECTION_NAMES, profile_to_section +from . import ( + PREDEFINED_SECTION_NAMES, + SUBSECTION_TYPE_ALLOWLIST, + get_section_header, + profile_to_section, +) class ConfigureSetCommand(BasicCommand): @@ -41,6 +49,20 @@ class ConfigureSetCommand(BasicCommand): 'cli_type_name': 'string', 'positional_arg': True, }, + { + 'name': 'sso-session', + 'help_text': 'The name of the SSO session sub-section to configure.', + 'action': 'store', + 'cli_type_name': 'string', + 'group_name': 'subsection', + }, + { + 'name': 'services', + 'help_text': 'The name of the services sub-section to configure.', + 'action': 'store', + 'cli_type_name': 'string', + 'group_name': 'subsection', + }, ] # Any variables specified in this list will be written to # the ~/.aws/credentials file instead of ~/.aws/config. @@ -60,10 +82,60 @@ def _get_config_file(self, path): config_path = self._session.get_config_variable(path) return os.path.expanduser(config_path) + def _get_subsection_from_args(self, args): + # Validate mutual exclusivity of sub-section type parameters + groups = [[value['param_name']] for value in SUBSECTION_TYPE_ALLOWLIST.values()] + validate_mutually_exclusive(args, *groups) + + subsection_name = None + subsection_type = None + + for section_type, section_properties in SUBSECTION_TYPE_ALLOWLIST.items(): + if hasattr(args, section_properties['param_name']): + param_value = getattr(args, section_properties['param_name']) + if param_value is not None: + if not re.match(r"[\w\d_\-/.%@:\+]+", param_value): + raise ParamValidationError( + f"aws: [ERROR]: Invalid value for --{section_type}." + ) + subsection_name = param_value + subsection_type = section_type + break + + return (subsection_type, subsection_name) + + + def _set_subsection_property(self, section_type, section_name, varname, value): + if '.' in varname: + parts = varname.split('.') + if len(parts) > 2: + return 0 + + varname = parts[0] + value = {parts[1]: value} + + # Build update dict + updated_config = { + '__section__': get_section_header(section_type, section_name), + varname: value + } + + # Write to config file + config_filename = self._get_config_file('config_file') + self._config_writer.update_config(updated_config, config_filename) + + return 0 + def _run_main(self, args, parsed_globals): varname = args.varname value = args.value profile = 'default' + + section_type, section_name = self._get_subsection_from_args(args) + if section_type is not None: + return self._set_subsection_property(section_type, section_name, varname, value) + + # Not in a sub-section, continue with previous profile logic. # Before handing things off to the config writer, # we need to find out three things: # 1. What section we're writing to (profile). diff --git a/tests/functional/configure/test_configure.py b/tests/functional/configure/test_configure.py index b93c662e4879..7123aab948b8 100644 --- a/tests/functional/configure/test_configure.py +++ b/tests/functional/configure/test_configure.py @@ -299,6 +299,81 @@ def test_get_nested_attribute(self): ) self.assertEqual(stdout, "") + def test_set_with_subsection_no_name_provided(self): + _, stderr, _ = self.run_cmd( + [ + "configure", + "set", + "--sso-session", + '', + "space", + "test", + ], + expected_rc=252 + ) + self.assertIn("Invalid value for --sso-session", stderr) + + def test_set_deeply_nested_property_in_subsection_does_nothing(self): + self.set_config_file_contents( + "[services my-services]\n" "ec2 =\n" " endpoint_url = localhost\n" + ) + + self.run_cmd( + [ + "configure", + "set", + "--services", + 'my-services', + "s3.express.endpoint_url", + "localhost", + ], + expected_rc=0 + ) + self.assertEqual( + "[services my-services]\n" "ec2 =\n" " endpoint_url = localhost\n", + self.get_config_file_contents(), + ) + + def test_set_with_two_subsections_specified_results_in_error(self): + _, stderr, _ = self.run_cmd( + [ + "configure", + "set", + "--sso-session", + 'my-sso', + "--services", + 'my-services', + "space", + "test", + ], + expected_rc=252 + ) + self.assertIn( + "cannot be specified when one of the following keys are also specified:", + stderr + ) + + def test_set_updates_existing_property_in_subsection(self): + self.set_config_file_contents( + "[sso-session my-sso-session]\n" "sso_region = us-west-2\n" + ) + + self.run_cmd( + [ + "configure", + "set", + "--sso-session", + 'my-sso-session', + "sso_region", + "eu-central-1", + ], + expected_rc=0 + ) + self.assertEqual( + "[sso-session my-sso-session]\n" "sso_region = eu-central-1\n", + self.get_config_file_contents(), + ) + class TestConfigureHasArgTable(unittest.TestCase): def test_configure_command_has_arg_table(self): diff --git a/tests/unit/customizations/configure/test_set.py b/tests/unit/customizations/configure/test_set.py index 66a2ea2176d5..fb443844c972 100644 --- a/tests/unit/customizations/configure/test_set.py +++ b/tests/unit/customizations/configure/test_set.py @@ -205,3 +205,28 @@ def test_configure_set_with_profile_with_tab_dotted(self): {'__section__': "profile 'some\tprofile'", 'region': 'us-west-2'}, 'myconfigfile', ) + + def test_set_top_level_property_in_subsection(self): + set_command = ConfigureSetCommand(self.session, self.config_writer) + set_command( + args=['sso_region', 'us-west-2', '--sso-session', 'my-session'], + parsed_globals=None, + ) + self.config_writer.update_config.assert_called_with( + {'__section__': 'sso-session my-session', 'sso_region': 'us-west-2'}, + 'myconfigfile', + ) + + def test_set_nested_property_in_subsection(self): + set_command = ConfigureSetCommand(self.session, self.config_writer) + set_command( + args=['--services', 'my-services', 's3.endpoint_url', 'http://localhost:4566'], + parsed_globals=None, + ) + self.config_writer.update_config.assert_called_with( + { + '__section__': 'services my-services', + 's3': {'endpoint_url': 'http://localhost:4566'}, + }, + 'myconfigfile', + ) From 827edb3395a7ff590f81728bd9ea1a2e328f2f4c Mon Sep 17 00:00:00 2001 From: Kenneth Daily Date: Mon, 16 Mar 2026 10:53:06 -0700 Subject: [PATCH 2/5] Improved readability of subsection valdiation Remove the mapping between the subsection type and the parameter name. This is a transformation between '-' separated and '_' separated, so use a utility method to do this. Rename parameters to improve understanding of the code. --- awscli/customizations/configure/__init__.py | 6 ++---- awscli/customizations/configure/set.py | 17 ++++++++++------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/awscli/customizations/configure/__init__.py b/awscli/customizations/configure/__init__.py index 990d53afb4e7..c8ea5b6fa999 100644 --- a/awscli/customizations/configure/__init__.py +++ b/awscli/customizations/configure/__init__.py @@ -15,15 +15,13 @@ NOT_SET = '' PREDEFINED_SECTION_NAMES = 'plugins' -# A map between the command line parameter name and the Python object name -# For allowed sub-section types +# A map between the command line parameter name and the name used +# in the full config object. SUBSECTION_TYPE_ALLOWLIST = { 'sso-session': { - "param_name" :'sso_session', "full_config_name": "sso_sessions" }, 'services': { - 'param_name': 'services', "full_config_name": "services" }, } diff --git a/awscli/customizations/configure/set.py b/awscli/customizations/configure/set.py index 4400693aeeda..a1308f47d0b8 100644 --- a/awscli/customizations/configure/set.py +++ b/awscli/customizations/configure/set.py @@ -82,23 +82,26 @@ def _get_config_file(self, path): config_path = self._session.get_config_variable(path) return os.path.expanduser(config_path) + def _subsection_parameter_to_argument_name(self, parameter_name): + return parameter_name.replace("-", "_") + def _get_subsection_from_args(self, args): # Validate mutual exclusivity of sub-section type parameters - groups = [[value['param_name']] for value in SUBSECTION_TYPE_ALLOWLIST.values()] + groups = [[self._subsection_parameter_to_argument_name(key)] for key in SUBSECTION_TYPE_ALLOWLIST.keys()] validate_mutually_exclusive(args, *groups) subsection_name = None subsection_type = None - for section_type, section_properties in SUBSECTION_TYPE_ALLOWLIST.items(): - if hasattr(args, section_properties['param_name']): - param_value = getattr(args, section_properties['param_name']) - if param_value is not None: - if not re.match(r"[\w\d_\-/.%@:\+]+", param_value): + for section_type in SUBSECTION_TYPE_ALLOWLIST.keys(): + cli_parameter_name = self._subsection_parameter_to_argument_name(section_type) + if hasattr(args, cli_parameter_name): + subsection_name = getattr(args, cli_parameter_name) + if subsection_name is not None: + if not re.match(r"[\w\d_\-/.%@:\+]+", subsection_name): raise ParamValidationError( f"aws: [ERROR]: Invalid value for --{section_type}." ) - subsection_name = param_value subsection_type = section_type break From c1a63ba68f268b8f5fd90753cc36ee7727fa237e Mon Sep 17 00:00:00 2001 From: Kenneth Daily Date: Mon, 16 Mar 2026 10:54:48 -0700 Subject: [PATCH 3/5] Return error if configuring deeply nested properties Only one level of nested properties is supported, e.g. 'aaa.bbb'. If the user supplies more than that (.aaa.bbb.ccc') an error is raised. --- awscli/customizations/configure/set.py | 8 ++++++-- tests/functional/configure/test_configure.py | 12 ++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/awscli/customizations/configure/set.py b/awscli/customizations/configure/set.py index a1308f47d0b8..72a12cc5fa92 100644 --- a/awscli/customizations/configure/set.py +++ b/awscli/customizations/configure/set.py @@ -111,9 +111,13 @@ def _get_subsection_from_args(self, args): def _set_subsection_property(self, section_type, section_name, varname, value): if '.' in varname: parts = varname.split('.') + # Check if there are more than two parts to the property name to set (e.g., aaa.bbb.ccc) + # This would result in a deeply nested property, which is not supported. if len(parts) > 2: - return 0 - + raise ParamValidationError( + "Found more than two parts in the property to set. " + "Deep nesting of properties is not supported." + ) varname = parts[0] value = {parts[1]: value} diff --git a/tests/functional/configure/test_configure.py b/tests/functional/configure/test_configure.py index 7123aab948b8..fc53f68e37fb 100644 --- a/tests/functional/configure/test_configure.py +++ b/tests/functional/configure/test_configure.py @@ -313,12 +313,12 @@ def test_set_with_subsection_no_name_provided(self): ) self.assertIn("Invalid value for --sso-session", stderr) - def test_set_deeply_nested_property_in_subsection_does_nothing(self): + def test_set_deeply_nested_property_in_subsection_results_in_error(self): self.set_config_file_contents( "[services my-services]\n" "ec2 =\n" " endpoint_url = localhost\n" ) - self.run_cmd( + _, stderr, _ = self.run_cmd( [ "configure", "set", @@ -327,11 +327,11 @@ def test_set_deeply_nested_property_in_subsection_does_nothing(self): "s3.express.endpoint_url", "localhost", ], - expected_rc=0 + expected_rc=252 ) - self.assertEqual( - "[services my-services]\n" "ec2 =\n" " endpoint_url = localhost\n", - self.get_config_file_contents(), + self.assertIn( + "Found more than two parts in the property to set.", + stderr ) def test_set_with_two_subsections_specified_results_in_error(self): From 1271886653003f7b48cc87d99a8642cb7d1d0b22 Mon Sep 17 00:00:00 2001 From: Kenneth Daily Date: Mon, 16 Mar 2026 11:09:28 -0700 Subject: [PATCH 4/5] Update documentation string for sub-section name parameter --- awscli/customizations/configure/set.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awscli/customizations/configure/set.py b/awscli/customizations/configure/set.py index 72a12cc5fa92..6fb19fefd890 100644 --- a/awscli/customizations/configure/set.py +++ b/awscli/customizations/configure/set.py @@ -51,14 +51,14 @@ class ConfigureSetCommand(BasicCommand): }, { 'name': 'sso-session', - 'help_text': 'The name of the SSO session sub-section to configure.', + 'help_text': 'The name of the sub-section to configure.', 'action': 'store', 'cli_type_name': 'string', 'group_name': 'subsection', }, { 'name': 'services', - 'help_text': 'The name of the services sub-section to configure.', + 'help_text': 'The name of the sub-section to configure.', 'action': 'store', 'cli_type_name': 'string', 'group_name': 'subsection', From be92fb10650637eb5e81aea62eb7a5e1c961e4ae Mon Sep 17 00:00:00 2001 From: Kenneth Daily Date: Mon, 16 Mar 2026 13:55:30 -0700 Subject: [PATCH 5/5] Added examples --- awscli/examples/configure/set/_examples.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/awscli/examples/configure/set/_examples.rst b/awscli/examples/configure/set/_examples.rst index 3b074b7f600d..b16b0b216d6f 100644 --- a/awscli/examples/configure/set/_examples.rst +++ b/awscli/examples/configure/set/_examples.rst @@ -59,3 +59,15 @@ will produce the following updated config file:: [profile testing2] region = us-west-2 cli_pager = + +To set a parameter in a sub-section, use one of the available sub-section parameters (``--services`` or ``--sso-session``). + +For example, to set the ``sso_start_url`` in the ``my-sso-sesssion`` SSO session sub-section, the following command can be used:: + + aws configure set --sso-session my-sso-session sso_start_url https://my-sso-portal.awsapps.com/start + +To set a nested property, use dotted notation for the parameter name along with a sub-section parameter. +For example, to set the a service specific endpoint URL for EC2 in a services sub-section called ``my-services``, the following command can be used:: + + aws configure set --services my-services ec2.endpoint_url http://localhost:4567 +