Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for "config get/set" command #603

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 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
5 changes: 4 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ CHANGELOG
Next Release (TBD)
==================

* Add support for ``configure get`` and ``configure set`` command which allow
you to set and get configuration values from the AWS config file
(`issue 602 <https://github.com/aws/aws-cli/issues/602`)
* Add support for the ``--recursive`` option in the ``aws s3 ls`` command
(`issue https://github.com/aws/aws-cli/issues/465`)
(`issue 465 <https://github.com/aws/aws-cli/issues/465`)
* Add support for the ``AWS_CA_BUNDLE`` environment variable so that users
can specify an alternate path to a cert bundle
(`issue 586 <https://github.com/aws/aws-cli/pull/586>`__)
Expand Down
10 changes: 9 additions & 1 deletion awscli/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ class CustomArgument(BaseCLIArgument):

def __init__(self, name, help_text='', dest=None, default=None,
action=None, required=None, choices=None, nargs=None,
cli_type_name=None, group_name=None):
cli_type_name=None, group_name=None, positional_arg=False):
self._name = name
self._help = help_text
self._dest = dest
Expand All @@ -172,6 +172,7 @@ def __init__(self, name, help_text='', dest=None, default=None,
self._nargs = nargs
self._cli_type_name = cli_type_name
self._group_name = group_name
self._positional_arg = positional_arg
if choices is None:
choices = []
self._choices = choices
Expand All @@ -181,6 +182,13 @@ def __init__(self, name, help_text='', dest=None, default=None,
# and docs code relies on this object.
self.argument_object = None

@property
def cli_name(self):
if self._positional_arg:
return self._name
else:
return '--' + self._name

def add_to_parser(self, parser):
"""

Expand Down
41 changes: 36 additions & 5 deletions awscli/customizations/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@
from awscli.help import HelpCommand


class _FromFile(object):
def __init__(self, *paths):
self.filename = None
if paths:
self.filename = os.path.join(*paths)


class BasicCommand(CLICommand):
"""Basic top level command with no subcommands.

Expand Down Expand Up @@ -57,7 +64,7 @@ class BasicCommand(CLICommand):
# The command_class must subclass from ``BasicCommand``.
SUBCOMMANDS = []

FROM_FILE = object()
FROM_FILE = _FromFile
# You can set the DESCRIPTION, SYNOPSIS, and EXAMPLES to FROM_FILE
# and we'll automatically read in that data from the file.
# This is useful if you have a lot of content and would prefer to keep
Expand All @@ -72,6 +79,17 @@ class BasicCommand(CLICommand):
# DESCRIPTION = awscli/examples/<command name>/_description.rst
# SYNOPSIS = awscli/examples/<command name>/_synopsis.rst
# EXAMPLES = awscli/examples/<command name>/_examples.rst
#
# You can also provide a relative path and we'll load the file
# from the specified location:
#
# DESCRIPTION = awscli/examples/<filename>
#
# For example:
#
# DESCRIPTION = FROM_FILE('command, 'subcommand, '_description.rst')
# DESCRIPTION = 'awscli/examples/command/subcommand/_description.rst'
#

# At this point, the only other thing you have to implement is a _run_main
# method (see the method for more information).
Expand All @@ -91,9 +109,9 @@ def __call__(self, args, parsed_globals):
elif getattr(parsed_args, 'subcommand', None) is None:
# No subcommand was specified so call the main
# function for this top level command.
self._run_main(parsed_args, parsed_globals)
return self._run_main(parsed_args, parsed_globals)
else:
subcommand_table[parsed_args.subcommand](remaining, parsed_globals)
return subcommand_table[parsed_args.subcommand](remaining, parsed_globals)

def _run_main(self, parsed_args, parsed_globals):
# Subclasses should implement this method.
Expand Down Expand Up @@ -175,10 +193,14 @@ def examples(self):

def _get_doc_contents(self, attr_name):
value = getattr(self, attr_name)
if value is BasicCommand.FROM_FILE:
if isinstance(value, BasicCommand.FROM_FILE):
if value.filename is not None:
trailing_path = value.filename
else:
trailing_path = os.path.join(self.name, attr_name + '.rst')
doc_path = os.path.join(
os.path.abspath(os.path.dirname(awscli.__file__)), 'examples',
self.name, attr_name + '.rst')
trailing_path)
with open(doc_path) as f:
return f.read()
else:
Expand Down Expand Up @@ -214,6 +236,15 @@ def doc_synopsis_start(self, help_command, **kwargs):
self.doc.style.start_codeblock()
self.doc.writeln(help_command.synopsis)

def doc_synopsis_option(self, arg_name, help_command, **kwargs):
if not help_command.synopsis:
super(BasicDocHandler, self).doc_synopsis_option(
help_command=help_command, **kwargs)
else:
# A synopsis has been provided so we don't need to write
# anything here.
pass

def doc_synopsis_end(self, help_command, **kwargs):
if not help_command.synopsis:
super(BasicDocHandler, self).doc_synopsis_end(
Expand Down
116 changes: 105 additions & 11 deletions awscli/customizations/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import sys
import logging

import six
from botocore.exceptions import ProfileNotFound

from awscli.customizations.commands import BasicCommand
Expand Down Expand Up @@ -103,7 +102,7 @@ def _create_file(self, config_filename):
if not os.path.isdir(dirname):
os.makedirs(dirname)
with os.fdopen(os.open(config_filename,
os.O_WRONLY|os.O_CREAT, 0o600), 'w') as f:
os.O_WRONLY|os.O_CREAT, 0o600), 'w'):
pass

def _write_new_section(self, section_name, new_values, config_filename):
Expand All @@ -115,9 +114,11 @@ def _write_new_section(self, section_name, new_values, config_filename):
def _update_section_contents(self, contents, section_name, new_values):
new_values = new_values.copy()
# contents is a list of file line contents.
start_index = 0
for i in range(len(contents)):
line = contents[i]
if line.strip().startswith(('#', ';')):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL: You can provide a tuple to .startswith(...).

mind_blown

# This is a comment, so we can safely ignore this line.
continue
match = self.SECTION_REGEX.search(line)
if match is not None and self._matches_section(match,
section_name):
Expand Down Expand Up @@ -259,21 +260,113 @@ def _lookup_credentials(self):

def _lookup_config(self, name):
# First try to look up the variable in the env.
value = self._session.get_variable(name, methods=('env',))
value = self._session.get_config_variable(name, methods=('env',))
if value is not None:
return ConfigValue(value, 'env', self._session.env_vars[name][1])
return ConfigValue(value, 'env', self._session.session_var_map[name][1])
# Then try to look up the variable in the config file.
value = self._session.get_variable(name, methods=('config',))
value = self._session.get_config_variable(name, methods=('config',))
if value is not None:
return ConfigValue(value, 'config_file',
self._session.get_variable('config_file'))
self._session.get_config_variable('config_file'))
else:
return ConfigValue(NOT_SET, None, None)

class ConfigureSetCommand(BasicCommand):
NAME = 'set'
DESCRIPTION = BasicCommand.FROM_FILE('configure', 'set',
'_description.rst')
SYNOPSIS = ('aws configure set varname value [--profile profile-name]')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the parens here? It's not a tuple (no trailing comma)...

EXAMPLES = BasicCommand.FROM_FILE('configure', 'set', '_examples.rst')
ARG_TABLE = [
{'name': 'varname',
'help_text': 'The name of the config value to set.',
'action': 'store',
'cli_type_name': 'string', 'positional_arg': True},
{'name': 'value',
'help_text': 'The value to set.',
'action': 'store',
'cli_type_name': 'string', 'positional_arg': True},
]

def __init__(self, session, config_writer=None):
super(ConfigureSetCommand, self).__init__(session)
if config_writer is None:
config_writer = ConfigFileWriter()
self._config_writer = config_writer

def _run_main(self, args, parsed_globals):
varname = args.varname
value = args.value
section = 'default'
if '.' not in varname:
if self._session.profile is not None:
section = 'profile %s' % self._session.profile
else:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this block for per-service config options? Or something else? A comment explaining what's going on would be nice.

num_dots = varname.count('.')
if num_dots == 1:
section, varname = varname.split('.')
elif num_dots == 2 and varname.startswith('profile'):
dotted_section, varname = varname.rsplit('.', 1)
profile = dotted_section.split('.')[1]
section = 'profile %s' % profile
config_filename = os.path.expanduser(
self._session.get_config_variable('config_file'))
updated_config = {'__section__': section, varname: value}
self._config_writer.update_config(updated_config, config_filename)


class ConfigureGetCommand(BasicCommand):
NAME = 'get'
DESCRIPTION = BasicCommand.FROM_FILE('configure', 'get',
'_description.rst')
SYNOPSIS = ('aws configure get varname [--profile profile-name]')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, parens but not a tuple?

EXAMPLES = BasicCommand.FROM_FILE('configure', 'get', '_examples.rst')
ARG_TABLE = [
{'name': 'varname',
'help_text': 'The name of the config value to retrieve.',
'action': 'store',
'cli_type_name': 'string', 'positional_arg': True},
]

def __init__(self, session, stream=sys.stdout):
super(ConfigureGetCommand, self).__init__(session)
self._stream = stream

def _run_main(self, args, parsed_globals):
varname = args.varname
value = None
if '.' not in varname:
# get_config() returns the config variables in the config
# file (not the logical_var names), which is what we want.
config = self._session.get_config()
value = config.get(varname)
else:
num_dots = varname.count('.')
if num_dots == 1:
full_config = self._session.full_config
section, config_name = varname.split('.')
value = full_config.get(section, {}).get(config_name)
elif num_dots == 2 and varname.startswith('profile'):
# We're hard coding logic for profiles here. Really
# we could support any generic format of [section subsection],
# but we'd need some botocore.session changes for that,
# and nothing would immediately use that feature.
dot_section, config_name = varname.rsplit('.', 1)
start, profile_name = dot_section.split('.')
self._session.profile = profile_name
config = self._session.get_config()
value = config.get(config_name)
if value is not None:
self._stream.write(value)
self._stream.write('\n')
return 0
else:
return 1


class ConfigureCommand(BasicCommand):
NAME = 'configure'
DESCRIPTION = BasicCommand.FROM_FILE
DESCRIPTION = BasicCommand.FROM_FILE()
SYNOPSIS = ('aws configure [--profile profile-name]')
EXAMPLES = (
'To create a new configuration::\n'
Expand All @@ -293,7 +386,9 @@ class ConfigureCommand(BasicCommand):
' Default output format [None]:\n'
)
SUBCOMMANDS = [
{'name': 'list', 'command_class': ConfigureListCommand}
{'name': 'list', 'command_class': ConfigureListCommand},
{'name': 'get', 'command_class': ConfigureGetCommand},
{'name': 'set', 'command_class': ConfigureSetCommand},
]

# If you want to add new values to prompt, update this list here.
Expand All @@ -318,7 +413,6 @@ def _run_main(self, parsed_args, parsed_globals):
# Called when invoked with no args "aws configure"
new_values = {}
if parsed_globals.profile is not None:
profile = parsed_globals.profile
self._session.profile = parsed_globals.profile
# This is the config from the config file scoped to a specific
# profile.
Expand All @@ -333,7 +427,7 @@ def _run_main(self, parsed_args, parsed_globals):
if new_value is not None and new_value != current_value:
new_values[config_name] = new_value
config_filename = os.path.expanduser(
self._session.get_variable('config_file'))
self._session.get_config_variable('config_file'))
if new_values:
if parsed_globals.profile is not None:
new_values['__section__'] = (
Expand Down
39 changes: 39 additions & 0 deletions awscli/examples/configure/get/_description.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
Get a configuration value from the config file.

The ``aws configure get`` command can be used to print a configuration value in
the AWS config file. The ``get`` command supports two types of configuration
values, *unqualified* and *qualified* config values.


Note that ``aws configure get`` only looks at values in the AWS configuration
file. It does **not** resolve configuration variables specified anywhere else,
including environment variables, command line arguments, etc.


Unqualified Names
-----------------

Every value in the AWS configuration file must be placed in a section (denoted
by ``[section-name]`` in the config file). To retrieve a value from the
config file, the section name and the config name must be known.

An unqualified configuration name refers to a name that is not scoped to a
specific section in the configuration file. Sections are specified by
separating parts with the ``"."`` character (``section.config-name``). An
unqualified name will be scoped to the current profile. For example,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth providing a dotted example here as well?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind, I just saw them in the examples below.

``aws configure get aws_access_key_id`` will retrieve the ``aws_access_key_id``
from the current profile, or the ``default`` profile if no profile is
specified. You can still provide a ``--profile`` argument to the ``aws
configure get`` command. For example, ``aws configure get region --profile
testing`` will print the region value for the ``testing`` profile.


Qualified Names
---------------

A qualified name is a name that has at least one ``"."`` character in the name.
This name provides a way to specify the config section from which to retrieve
the config variable. When a qualified name is provided to ``aws configure
get``, the currently specified profile is ignored. Section names that have
the format ``[profile profile-name]`` can be specified by using the
``profile.profile-name.config-value`` syntax.
35 changes: 35 additions & 0 deletions awscli/examples/configure/get/_examples.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
Suppose you had the following config file::

[default]
aws_access_key_id=default_access_key
aws_secret_access_key=default_secret_key

[preview]
cloudsearch=true

[profile testing]
aws_access_key_id=testing_access_key
aws_secret_access_key=testing_secret_key
region=us-west-2

The following commands would have the corresponding output::

$ aws configure get aws_access_key_id
default_access_key

$ aws configure get default.aws_access_key_id
default_access_key

$ aws configure get aws_access_key_id --profile testing
testing_access_key

$ aws configure get profile.testing.aws_access_key_id
default_access_key

$ aws configure get preview.cloudsearch
true

$ aws configure get preview.does-not-exist
$
$ echo $?
1
13 changes: 13 additions & 0 deletions awscli/examples/configure/set/_description.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Set a configuration value from the config file.

The ``aws configure set`` command can be used to set a single configuration
value in the AWS config file. The ``set`` command supports both the
*qualified* and *unqualified* config values documented in the ``get`` command
(see ``aws configure get help`` for more information).

To set a single value, provide the configuration name followed by the
configuration value.

If the config file does not exist, one will automatically be created. If the
configuration value already exists in the config file, it will updated with the
new configuration value.
Loading