Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion awscli/customizations/configure/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,21 @@
# 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 = '<not set>'
PREDEFINED_SECTION_NAMES = 'plugins'
# A map between the command line parameter name and the name used
# in the full config object.
SUBSECTION_TYPE_ALLOWLIST = {
'sso-session': {
"full_config_name": "sso_sessions"
},
'services': {
"full_config_name": "services"
},
}
_WHITESPACE = ' \t'


Expand Down
81 changes: 80 additions & 1 deletion awscli/customizations/configure/set.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -41,6 +49,20 @@ class ConfigureSetCommand(BasicCommand):
'cli_type_name': 'string',
'positional_arg': True,
},
{
'name': 'sso-session',
'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 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.
Expand All @@ -60,10 +82,67 @@ 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 = [[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 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_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('.')
# 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:
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}

# 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).
Expand Down
12 changes: 12 additions & 0 deletions awscli/examples/configure/set/_examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

75 changes: 75 additions & 0 deletions tests/functional/configure/test_configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_results_in_error(self):
self.set_config_file_contents(
"[services my-services]\n" "ec2 =\n" " endpoint_url = localhost\n"
)

_, stderr, _ = self.run_cmd(
[
"configure",
"set",
"--services",
'my-services',
"s3.express.endpoint_url",
"localhost",
],
expected_rc=252
)
self.assertIn(
"Found more than two parts in the property to set.",
stderr
)

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):
Expand Down
25 changes: 25 additions & 0 deletions tests/unit/customizations/configure/test_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
)
Loading