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 profile support #92

Closed
wants to merge 4 commits into from
Closed
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
42 changes: 42 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,33 @@ you can try::
TABLENAMES Second
TABLENAMES Third

Profiles
--------

The aws-shell supports AWS CLI profiles. You have two options to use
profiles. First, you can provide a profile when you start the aws-shell::

$ aws-shell --profile prod
aws>

When you do this all the server side completion as well as CLI commands
you run will automatically use the ``prod`` profile.

You can also change the current profile while you're in the aws-shell::

$ aws-shell
aws> .profile demo
Current shell profile changed to: demo

You can also check what profile you've configured in the aws-shell using::

aws> .profile
Current shell profile: demo

After changing your profile using the ``.profile`` dot command, all
server side completion as well as CLI commands will automatically use
the new profile you've configured.


Features
========
Expand Down Expand Up @@ -218,6 +245,21 @@ variable before defaulting to ``notepad`` on Windows and
aws> dynamodb list-tables
aws> .edit

Changing Profiles with .profile
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

You can change the current AWS CLI profile used by the aws-shell
by using the ``.profile`` dot command. If you run the ``.profile``
command with no arguments, the currently configured shell profile
will be printed.

::

aws> .profile demo
Current shell profile changed to: demo
aws> .profile
Current shell profile: demo


Executing Shell Commands
------------------------
Expand Down
8 changes: 8 additions & 0 deletions awsshell/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import unicode_literals, print_function

import json
import argparse
import threading

from awsshell import shellcomplete
Expand Down Expand Up @@ -28,6 +29,11 @@ def load_index(filename):


def main():
parser = argparse.ArgumentParser()
parser.add_argument('-p', '--profile', help='The profile name to use '
'when starting the AWS Shell.')
args = parser.parse_args()

indexer = completion.CompletionIndex()
try:
index_str = indexer.load_index(utils.AWSCLI_VERSION)
Expand Down Expand Up @@ -59,6 +65,8 @@ def main():
model_completer = autocomplete.AWSCLIModelCompleter(index_data)
completer = shellcomplete.AWSShellCompleter(model_completer)
shell = app.create_aws_shell(completer, model_completer, doc_data)
if args.profile:
shell.profile = args.profile
shell.run()


Expand Down
64 changes: 63 additions & 1 deletion awsshell/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,45 @@ def run(self, command, application):
p.communicate()


class ProfileHandler(object):
USAGE = (
'.profile # Print the current profile\n'
'.profile <name> # Change the current profile\n'
)

def __init__(self, output=sys.stdout, err=sys.stderr):
self._output = output
self._err = err

def run(self, command, application):
"""Get or set the profile.

If .profile is called with no args, the current profile
is displayed. If the .profile command is called with a
single arg, then the current profile for the application
will be set to the new value.
"""
if len(command) == 1:
profile = application.profile
if profile is None:
self._output.write(
"Current shell profile: no profile configured\n"
"You can change profiles using: .profile profile-name\n")
else:
self._output.write("Current shell profile: %s\n" % profile)
elif len(command) == 2:
new_profile_name = command[1]
application.profile = new_profile_name
self._output.write("Current shell profile changed to: %s\n" %
new_profile_name)
else:
self._err.write("Usage:\n%s\n" % self.USAGE)


class DotCommandHandler(object):
HANDLER_CLASSES = {
'edit': EditHandler,
'profile': ProfileHandler,
}

def __init__(self, output=sys.stdout, err=sys.stderr):
Expand Down Expand Up @@ -162,6 +198,8 @@ def __init__(self, completer, model_completer, docs):
self.refresh_cli = False
self.key_manager = None
self._dot_cmd = DotCommandHandler()
self._env = os.environ.copy()
self._profile = None

# These attrs come from the config file.
self.config_obj = None
Expand Down Expand Up @@ -233,7 +271,7 @@ def run(self):
initial_document=Document(self.current_docs,
cursor_position=0))
self.cli.request_redraw()
p = subprocess.Popen(full_cmd, shell=True)
p = subprocess.Popen(full_cmd, shell=True, env=self._env)
p.communicate()

def stop_input_and_refresh_cli(self):
Expand Down Expand Up @@ -386,3 +424,27 @@ def create_cli_interface(self, display_completions_in_columns):
display_completions_in_columns)
cli = CommandLineInterface(application=app, eventloop=loop)
return cli

@property
def profile(self):
return self._profile

@profile.setter
def profile(self, new_profile_name):
# There's only two things that need to know about new profile
# changes.
#
# First, the actual command runner. If we want
# to use a different profile, it should ensure that the CLI
# commands that get run use the new profile (via the
# AWS_DEFAULT_PROFILE env var).
#
# Second, we also need to let the server side autocompleter know.
#
# Given this is easy to manage by hand, I don't think
# it's worth adding an event system or observers just yet.
# If this gets hard to manage, the complexity of those systems
# would be worth it.
self._env['AWS_DEFAULT_PROFILE'] = new_profile_name
self.completer.change_profile(new_profile_name)
self._profile = new_profile_name
9 changes: 8 additions & 1 deletion awsshell/shellcomplete.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
"""
import os
import logging

import botocore.session
from prompt_toolkit.completion import Completer, Completion

from awsshell import fuzzy


Expand All @@ -33,7 +36,6 @@ def __init__(self, completer, server_side_completer=None):
self._server_side_completer = server_side_completer

def _create_server_side_completer(self, session=None):
import botocore.session
from awsshell.resource import index
if session is None:
session = botocore.session.Session()
Expand All @@ -48,6 +50,11 @@ def _create_server_side_completer(self, session=None):
completer = index.ServerSideCompleter(client_creator, describer)
return completer

def change_profile(self, profile_name):
"""Change the profile used for server side completions."""
self._server_side_completer = self._create_server_side_completer(
session=botocore.session.Session(profile=profile_name))

@property
def completer(self):
return self._completer
Expand Down
51 changes: 51 additions & 0 deletions tests/unit/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@


from awsshell import app
from awsshell import shellcomplete
from awsshell import compat


Expand Down Expand Up @@ -38,7 +39,57 @@ def test_edit_handler():
assert command_run[0] == 'my-editor'


def test_profile_handler_prints_profile():
shell = mock.Mock(spec=app.AWSShell)
shell.profile = 'myprofile'
stdout = compat.StringIO()
handler = app.ProfileHandler(stdout)
handler.run(['.profile'], shell)
assert stdout.getvalue().strip() == 'Current shell profile: myprofile'


def test_profile_handler_when_no_profile_configured():
shell = mock.Mock(spec=app.AWSShell)
shell.profile = None
stdout = compat.StringIO()
handler = app.ProfileHandler(stdout)
handler.run(['.profile'], shell)
assert stdout.getvalue() == (
'Current shell profile: no profile configured\n'
'You can change profiles using: .profile profile-name\n'
)


def test_profile_command_changes_profile():
shell = mock.Mock(spec=app.AWSShell)
shell.profile = 'myprofile'
stdout = compat.StringIO()
handler = app.ProfileHandler(stdout)

handler.run(['.profile', 'newprofile'], shell)

assert shell.profile == 'newprofile'


def test_profile_prints_error_on_bad_syntax():
stderr = compat.StringIO()
handler = app.ProfileHandler(None, stderr)
handler.run(['.profile', 'a', 'b', 'c'], None)

# We don't really care about the exact usage message here,
# we just want to ensure usage was written to stderr.
assert 'Usage' in stderr.getvalue()


def test_prints_error_message_on_unknown_dot_command(errstream):
handler = app.DotCommandHandler(err=errstream)
handler.handle_cmd(".unknown foo bar", None)
assert errstream.getvalue() == "Unknown dot command: .unknown\n"


def test_delegates_to_complete_changing_profile():
completer = mock.Mock(spec=shellcomplete.AWSShellCompleter)
shell = app.AWSShell(completer, mock.Mock(), mock.Mock())
shell.profile = 'mynewprofile'
assert completer.change_profile.call_args == mock.call('mynewprofile')
assert shell.profile == 'mynewprofile'