Skip to content

Commit 5ad8dc6

Browse files
rongjina987Rong Jin
andauthored
Add warning for outdated SessionManagerPlugin version (aws#10520)
* Add warning for outdated SessionManagerPlugin version * Consolidate version check into meets_requirement --------- Co-authored-by: Rong Jin <rongjina@amazon.com>
1 parent 5a89cef commit 5ad8dc6

3 files changed

Lines changed: 205 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "enhancement",
3+
"category": "SSM SessionManager",
4+
"description": "Add warning message for outdated SessionManagerPlugin version"
5+
}

‎awscli/customizations/sessionmanager.py‎

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
import errno
1616
import os
1717
import re
18+
import sys
1819

1920
from subprocess import check_call, check_output
2021
from awscli.compat import ignore_user_entered_signals
2122
from awscli.clidriver import ServiceOperation, CLIOperationCaller
23+
from awscli.customizations.utils import uni_print
2224

2325
logger = logging.getLogger(__name__)
2426

@@ -29,6 +31,16 @@
2931
'session-manager-plugin-not-found'
3032
)
3133

34+
OUTDATED_PLUGIN_VERSION_MESSAGE = (
35+
'\n'
36+
'WARNING: An outdated SessionManagerPlugin version detected. '
37+
'Please upgrade it to the latest version. \n'
38+
'For more information, refer:\n'
39+
' https://docs.aws.amazon.com/systems-manager/latest/userguide/'
40+
'session-manager-working-with-install-plugin.html\n'
41+
'\n'
42+
)
43+
3244

3345
def register_ssm_session(event_handlers):
3446
event_handlers.register('building-command-table.ssm',
@@ -53,16 +65,24 @@ class VersionRequirement:
5365
def __init__(self, min_version):
5466
self.min_version = min_version
5567

56-
def meets_requirement(self, version):
68+
def meets_requirement(self, version, inclusive=False):
5769
ssm_plugin_version = self._sanitize_plugin_version(version)
5870
if self._is_valid_version(ssm_plugin_version):
5971
norm_version, norm_min_version = self._normalize(
6072
ssm_plugin_version, self.min_version
6173
)
74+
if inclusive:
75+
return norm_version >= norm_min_version
6276
return norm_version > norm_min_version
6377
else:
6478
return False
6579

80+
def is_valid_plugin_version(self, version):
81+
"""Check whether the reported version string is parseable."""
82+
return self._is_valid_version(
83+
self._sanitize_plugin_version(version)
84+
)
85+
6686
def _sanitize_plugin_version(self, plugin_version):
6787
return re.sub(self.WHITESPACE_REGEX, "", plugin_version)
6888

@@ -93,8 +113,26 @@ def create_help_command(self):
93113

94114
class StartSessionCaller(CLIOperationCaller):
95115
LAST_PLUGIN_VERSION_WITHOUT_ENV_VAR = "1.2.497.0"
116+
RECOMMENDED_MINIMUM_PLUGIN_VERSION = "1.2.764.0"
96117
DEFAULT_SSM_ENV_NAME = "AWS_SSM_START_SESSION_RESPONSE"
97118

119+
def _warn_if_plugin_version_is_outdated(self, plugin_version):
120+
"""Warn when the plugin is older than the recommended minimum."""
121+
version_requirement = VersionRequirement(
122+
min_version=self.RECOMMENDED_MINIMUM_PLUGIN_VERSION
123+
)
124+
if not version_requirement.is_valid_plugin_version(plugin_version):
125+
logger.debug(
126+
'Unable to parse SessionManagerPlugin version %r, skipping '
127+
'outdated version warning', plugin_version
128+
)
129+
return
130+
if version_requirement.meets_requirement(
131+
plugin_version, inclusive=True
132+
):
133+
return
134+
uni_print(OUTDATED_PLUGIN_VERSION_MESSAGE, sys.stderr)
135+
98136
def invoke(self, service_name, operation_name, parameters,
99137
parsed_globals):
100138
client = self._session.create_client(
@@ -126,6 +164,10 @@ def invoke(self, service_name, operation_name, parameters,
126164
)
127165
env = os.environ.copy()
128166

167+
# Warn, but do not fail, when the plugin is older than the
168+
# recommended minimum version.
169+
self._warn_if_plugin_version_is_outdated(plugin_version)
170+
129171
# Check if this plugin supports passing the start session response
130172
# as an environment variable name. If it does, it will set the
131173
# value to the response from the start_session operation to the env

‎tests/unit/customizations/test_sessionmanager.py‎

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import json
1616
import pytest
1717
import subprocess
18+
import sys
1819

1920
from awscli.customizations import sessionmanager
2021
from awscli.testutils import mock, unittest
@@ -427,3 +428,159 @@ def test_sanitize_plugin_version(self, version, expected_result):
427428
def test_is_valid_version(self, version, expected_result):
428429
assert expected_result == \
429430
self.version_requirement._is_valid_version(version)
431+
432+
433+
class TestRecommendedMinimumVersionRequirement:
434+
version_requirement = sessionmanager.VersionRequirement(
435+
min_version="1.2.764.0"
436+
)
437+
438+
@pytest.mark.parametrize(
439+
"version, expected_result",
440+
[
441+
# The first version with the capability must satisfy the check.
442+
("1.2.764.0", True),
443+
("1.2.764", True),
444+
("1.2.764.1", True),
445+
("1.2.765.0", True),
446+
("1.3", True),
447+
("2.0.0.0", True),
448+
("\r\n1.2. 764.0", True),
449+
# Anything below the threshold does not.
450+
("1.2.763.9", False),
451+
("1.2.763", False),
452+
("1.2.497.0", False),
453+
("1.2", False),
454+
("1", False),
455+
("0.9.999.9", False),
456+
# Unparseable versions never satisfy the check.
457+
("invalid_version", False),
458+
("", False),
459+
],
460+
)
461+
def test_meets_requirement_inclusive(self, version, expected_result):
462+
assert expected_result == \
463+
self.version_requirement.meets_requirement(
464+
version, inclusive=True
465+
)
466+
467+
@pytest.mark.parametrize(
468+
"version, expected_result",
469+
[
470+
("1.2.764.0", True),
471+
("\r\n1.2.764.0\n", True),
472+
("1.2.764", True),
473+
("invalid_version", False),
474+
("", False),
475+
("1.1.1.1.1", False),
476+
],
477+
)
478+
def test_is_valid_plugin_version(self, version, expected_result):
479+
assert expected_result == \
480+
self.version_requirement.is_valid_plugin_version(version)
481+
482+
483+
class TestOutdatedPluginVersionWarning(unittest.TestCase):
484+
485+
def setUp(self):
486+
self.session = mock.Mock(botocore.session.Session)
487+
self.client = mock.Mock()
488+
self.region = 'us-west-2'
489+
self.endpoint_url = 'testUrl'
490+
self.client.meta.region_name = self.region
491+
self.client.meta.endpoint_url = self.endpoint_url
492+
self.session.create_client.return_value = self.client
493+
self.caller = sessionmanager.StartSessionCaller(self.session)
494+
495+
self.parsed_globals = mock.Mock()
496+
self.parsed_globals.profile = 'user_profile'
497+
498+
self.start_session_params = {"Target": "i-123456789"}
499+
self.client.start_session.return_value = {
500+
"SessionId": "session-id",
501+
"TokenValue": "token-value",
502+
"StreamUrl": "stream-url",
503+
}
504+
505+
def _invoke_with_plugin_version(self, plugin_version):
506+
with mock.patch(
507+
'awscli.customizations.sessionmanager.check_output'
508+
) as mock_check_output, mock.patch(
509+
'awscli.customizations.sessionmanager.check_call'
510+
) as mock_check_call, mock.patch(
511+
'awscli.customizations.sessionmanager.uni_print'
512+
) as mock_uni_print:
513+
mock_check_output.return_value = plugin_version
514+
mock_check_call.return_value = 0
515+
rc = self.caller.invoke(
516+
'ssm', 'StartSession', self.start_session_params,
517+
self.parsed_globals
518+
)
519+
return rc, mock_uni_print
520+
521+
def _warning_messages(self, mock_uni_print):
522+
return [
523+
call_args[0][0] for call_args in mock_uni_print.call_args_list
524+
if call_args[0]
525+
and call_args[0][0] ==
526+
sessionmanager.OUTDATED_PLUGIN_VERSION_MESSAGE
527+
]
528+
529+
def test_warns_when_plugin_version_is_below_threshold(self):
530+
rc, mock_uni_print = self._invoke_with_plugin_version("1.2.763.0\n")
531+
# The warning is advisory only and must not fail the request.
532+
self.assertEqual(rc, 0)
533+
self.assertEqual(len(self._warning_messages(mock_uni_print)), 1)
534+
mock_uni_print.assert_called_with(
535+
sessionmanager.OUTDATED_PLUGIN_VERSION_MESSAGE, sys.stderr
536+
)
537+
538+
def test_warns_when_plugin_version_predates_env_var_support(self):
539+
rc, mock_uni_print = self._invoke_with_plugin_version("1.2.0.0\n")
540+
self.assertEqual(rc, 0)
541+
self.assertEqual(len(self._warning_messages(mock_uni_print)), 1)
542+
543+
def test_no_warning_at_exact_threshold_version(self):
544+
rc, mock_uni_print = self._invoke_with_plugin_version("1.2.764.0\n")
545+
self.assertEqual(rc, 0)
546+
self.assertEqual(self._warning_messages(mock_uni_print), [])
547+
548+
def test_no_warning_when_plugin_version_is_newer(self):
549+
rc, mock_uni_print = self._invoke_with_plugin_version("1.2.765.0\n")
550+
self.assertEqual(rc, 0)
551+
self.assertEqual(self._warning_messages(mock_uni_print), [])
552+
553+
def test_no_warning_when_plugin_version_is_unparseable(self):
554+
rc, mock_uni_print = self._invoke_with_plugin_version(
555+
"not_a_version\n"
556+
)
557+
self.assertEqual(rc, 0)
558+
self.assertEqual(self._warning_messages(mock_uni_print), [])
559+
560+
def test_outdated_plugin_still_receives_start_session_response(self):
561+
# An outdated plugin must keep the existing fallback behavior of
562+
# receiving the response directly rather than via an env var.
563+
with mock.patch(
564+
'awscli.customizations.sessionmanager.check_output'
565+
) as mock_check_output, mock.patch(
566+
'awscli.customizations.sessionmanager.check_call'
567+
) as mock_check_call, mock.patch(
568+
'awscli.customizations.sessionmanager.uni_print'
569+
):
570+
mock_check_output.return_value = "1.2.0.0\n"
571+
mock_check_call.return_value = 0
572+
rc = self.caller.invoke(
573+
'ssm', 'StartSession', self.start_session_params,
574+
self.parsed_globals
575+
)
576+
577+
self.assertEqual(rc, 0)
578+
check_call_args = mock_check_call.call_args[0][0]
579+
self.assertEqual(
580+
json.loads(check_call_args[1]),
581+
{
582+
"SessionId": "session-id",
583+
"TokenValue": "token-value",
584+
"StreamUrl": "stream-url",
585+
},
586+
)

0 commit comments

Comments
 (0)