Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "enhancement",
"category": "SSM SessionManager",
"description": "Add warning message for outdated SessionManagerPlugin version"
}
47 changes: 47 additions & 0 deletions awscli/customizations/sessionmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
import errno
import os
import re
import sys

from subprocess import check_call, check_output
from awscli.compat import ignore_user_entered_signals
from awscli.clidriver import ServiceOperation, CLIOperationCaller
from awscli.customizations.utils import uni_print

logger = logging.getLogger(__name__)

Expand All @@ -29,6 +31,15 @@
'session-manager-plugin-not-found'
)

OUTDATED_PLUGIN_VERSION_MESSAGE = (
'\n'
'WARNING: An outdated SessionManagerPlugin version detected. Please upgrade it to the latest version. \n'
'For more information, refer:\n'
Comment on lines +36 to +37

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

super nit - split this string into two lines for readability. The string value is unaffected

Suggested change
'WARNING: An outdated SessionManagerPlugin version detected. Please upgrade it to the latest version. \n'
'For more information, refer:\n'
'WARNING: An outdated SessionManagerPlugin version detected. '
'Please upgrade it to the latest version. \n'
'For more information, refer:\n'

' https://docs.aws.amazon.com/systems-manager/latest/userguide/'
'session-manager-working-with-install-plugin.html\n'
'\n'
)


def register_ssm_session(event_handlers):
event_handlers.register('building-command-table.ssm',
Expand Down Expand Up @@ -63,6 +74,22 @@ def meets_requirement(self, version):
else:
return False

def meets_or_exceeds(self, version):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This function seems to implement identical behavior to meets_requirements, but is inclusive. Can we just update meets_requirements to accept an inclusive parameter that defaults to false: meets_requirement(version, inclusive=False). Then you could just do:

...
if inclusive:
    return norm_version >= norm_min_version
return norm_version > norm_min_version

"""Check a version against ``min_version`` inclusively."""
ssm_plugin_version = self._sanitize_plugin_version(version)
if not self._is_valid_version(ssm_plugin_version):
return False
norm_version, norm_min_version = self._normalize(
ssm_plugin_version, self.min_version
)
return norm_version >= norm_min_version

def is_valid_plugin_version(self, version):
"""Check whether the reported version string is parseable."""
return self._is_valid_version(
self._sanitize_plugin_version(version)
)

def _sanitize_plugin_version(self, plugin_version):
return re.sub(self.WHITESPACE_REGEX, "", plugin_version)

Expand Down Expand Up @@ -93,8 +120,24 @@ def create_help_command(self):

class StartSessionCaller(CLIOperationCaller):
LAST_PLUGIN_VERSION_WITHOUT_ENV_VAR = "1.2.497.0"
RECOMMENDED_MINIMUM_PLUGIN_VERSION = "1.2.764.0"
DEFAULT_SSM_ENV_NAME = "AWS_SSM_START_SESSION_RESPONSE"

def _warn_if_plugin_version_is_outdated(self, plugin_version):
"""Warn when the plugin is older than the recommended minimum."""
version_requirement = VersionRequirement(
min_version=self.RECOMMENDED_MINIMUM_PLUGIN_VERSION
)
if not version_requirement.is_valid_plugin_version(plugin_version):
logger.debug(
'Unable to parse SessionManagerPlugin version %r, skipping '
'outdated version warning', plugin_version
)
return
if version_requirement.meets_or_exceeds(plugin_version):
return
uni_print(OUTDATED_PLUGIN_VERSION_MESSAGE, sys.stderr)

def invoke(self, service_name, operation_name, parameters,
parsed_globals):
client = self._session.create_client(
Expand Down Expand Up @@ -126,6 +169,10 @@ def invoke(self, service_name, operation_name, parameters,
)
env = os.environ.copy()

# Warn, but do not fail, when the plugin is older than the
# recommended minimum version.
self._warn_if_plugin_version_is_outdated(plugin_version)

# Check if this plugin supports passing the start session response
# as an environment variable name. If it does, it will set the
# value to the response from the start_session operation to the env
Expand Down
155 changes: 155 additions & 0 deletions tests/unit/customizations/test_sessionmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import json
import pytest
import subprocess
import sys

from awscli.customizations import sessionmanager
from awscli.testutils import mock, unittest
Expand Down Expand Up @@ -427,3 +428,157 @@ def test_sanitize_plugin_version(self, version, expected_result):
def test_is_valid_version(self, version, expected_result):
assert expected_result == \
self.version_requirement._is_valid_version(version)


class TestRecommendedMinimumVersionRequirement:
version_requirement = sessionmanager.VersionRequirement(
min_version="1.2.764.0"
)

@pytest.mark.parametrize(
"version, expected_result",
[
# The first version with the capability must satisfy the check.
("1.2.764.0", True),
("1.2.764", True),
("1.2.764.1", True),
("1.2.765.0", True),
("1.3", True),
("2.0.0.0", True),
("\r\n1.2. 764.0", True),
# Anything below the threshold does not.
("1.2.763.9", False),
("1.2.763", False),
("1.2.497.0", False),
("1.2", False),
("1", False),
("0.9.999.9", False),
# Unparseable versions never satisfy the check.
("invalid_version", False),
("", False),
],
)
def test_meets_or_exceeds(self, version, expected_result):
assert expected_result == \
self.version_requirement.meets_or_exceeds(version)

@pytest.mark.parametrize(
"version, expected_result",
[
("1.2.764.0", True),
("\r\n1.2.764.0\n", True),
("1.2.764", True),
("invalid_version", False),
("", False),
("1.1.1.1.1", False),
],
)
def test_is_valid_plugin_version(self, version, expected_result):
assert expected_result == \
self.version_requirement.is_valid_plugin_version(version)


class TestOutdatedPluginVersionWarning(unittest.TestCase):

def setUp(self):
self.session = mock.Mock(botocore.session.Session)
self.client = mock.Mock()
self.region = 'us-west-2'
self.endpoint_url = 'testUrl'
self.client.meta.region_name = self.region
self.client.meta.endpoint_url = self.endpoint_url
self.session.create_client.return_value = self.client
self.caller = sessionmanager.StartSessionCaller(self.session)

self.parsed_globals = mock.Mock()
self.parsed_globals.profile = 'user_profile'

self.start_session_params = {"Target": "i-123456789"}
self.client.start_session.return_value = {
"SessionId": "session-id",
"TokenValue": "token-value",
"StreamUrl": "stream-url",
}

def _invoke_with_plugin_version(self, plugin_version):
with mock.patch(
'awscli.customizations.sessionmanager.check_output'
) as mock_check_output, mock.patch(
'awscli.customizations.sessionmanager.check_call'
) as mock_check_call, mock.patch(
'awscli.customizations.sessionmanager.uni_print'
) as mock_uni_print:
mock_check_output.return_value = plugin_version
mock_check_call.return_value = 0
rc = self.caller.invoke(
'ssm', 'StartSession', self.start_session_params,
self.parsed_globals
)
return rc, mock_uni_print

def _warning_messages(self, mock_uni_print):
return [
call_args[0][0] for call_args in mock_uni_print.call_args_list
if call_args[0]
and call_args[0][0] ==
sessionmanager.OUTDATED_PLUGIN_VERSION_MESSAGE
]

def test_warns_when_plugin_version_is_below_threshold(self):
rc, mock_uni_print = self._invoke_with_plugin_version("1.2.763.0\n")
# The warning is advisory only and must not fail the request.
self.assertEqual(rc, 0)
self.assertEqual(len(self._warning_messages(mock_uni_print)), 1)
mock_uni_print.assert_called_with(
sessionmanager.OUTDATED_PLUGIN_VERSION_MESSAGE, sys.stderr
)

def test_warns_when_plugin_version_predates_env_var_support(self):
rc, mock_uni_print = self._invoke_with_plugin_version("1.2.0.0\n")
self.assertEqual(rc, 0)
self.assertEqual(len(self._warning_messages(mock_uni_print)), 1)

def test_no_warning_at_exact_threshold_version(self):
rc, mock_uni_print = self._invoke_with_plugin_version("1.2.764.0\n")
self.assertEqual(rc, 0)
self.assertEqual(self._warning_messages(mock_uni_print), [])

def test_no_warning_when_plugin_version_is_newer(self):
rc, mock_uni_print = self._invoke_with_plugin_version("1.2.765.0\n")
self.assertEqual(rc, 0)
self.assertEqual(self._warning_messages(mock_uni_print), [])

def test_no_warning_when_plugin_version_is_unparseable(self):
rc, mock_uni_print = self._invoke_with_plugin_version(
"not_a_version\n"
)
self.assertEqual(rc, 0)
self.assertEqual(self._warning_messages(mock_uni_print), [])

def test_outdated_plugin_still_receives_start_session_response(self):
# An outdated plugin must keep the existing fallback behavior of
# receiving the response directly rather than via an env var.
with mock.patch(
'awscli.customizations.sessionmanager.check_output'
) as mock_check_output, mock.patch(
'awscli.customizations.sessionmanager.check_call'
) as mock_check_call, mock.patch(
'awscli.customizations.sessionmanager.uni_print'
):
mock_check_output.return_value = "1.2.0.0\n"
mock_check_call.return_value = 0
rc = self.caller.invoke(
'ssm', 'StartSession', self.start_session_params,
self.parsed_globals
)

self.assertEqual(rc, 0)
check_call_args = mock_check_call.call_args[0][0]
self.assertEqual(
json.loads(check_call_args[1]),
{
"SessionId": "session-id",
"TokenValue": "token-value",
"StreamUrl": "stream-url",
},
)