Skip to content

Commit

Permalink
CLI CUSTOMIZATION FOR ECS EXECUTE-COMMAND API
Browse files Browse the repository at this point in the history
* ECS execute-command API returns a streamUrl and a token-value using
 a user can connect to a session inside a running container by calling
 session-manager-plugin with those values.
* This CLI customization makes this call for the user, thus internally
 making two calls.
  • Loading branch information
Mansi Dabhole authored and nateprewitt committed Mar 15, 2021
1 parent cab1b4d commit ba4163d
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 1 deletion.
11 changes: 10 additions & 1 deletion awscli/customizations/ecs/__init__.py
Expand Up @@ -12,6 +12,8 @@
# language governing permissions and limitations under the License.

from awscli.customizations.ecs.deploy import ECSDeploy
from awscli.customizations.ecs.executeCommand import EcsExecuteCommand
from awscli.customizations.ecs.executeCommand import ExecuteCommandCaller

def initialize(cli):
"""
Expand All @@ -24,4 +26,11 @@ def inject_commands(command_table, session, **kwargs):
Called when the ECS command table is being built. Used to inject new
high level commands into the command list.
"""
command_table['deploy'] = ECSDeploy(session)
command_table['deploy'] = ECSDeploy(session)
command_table['execute-command'] = EcsExecuteCommand(
name='execute-command',
parent_name='ecs',
session=session,
operation_model=session.get_service_model('ecs').operation_model('ExecuteCommand'),
operation_caller=ExecuteCommandCaller(session),
)
71 changes: 71 additions & 0 deletions awscli/customizations/ecs/executeCommand.py
@@ -0,0 +1,71 @@
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# 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 logging
import json
import errno

from subprocess import check_call
from awscli.compat import ignore_user_entered_signals
from awscli.clidriver import ServiceOperation, CLIOperationCaller

logger = logging.getLogger(__name__)

ERROR_MESSAGE = (
'SessionManagerPlugin is not found. ',
'Please refer to SessionManager Documentation here: ',
'http://docs.aws.amazon.com/console/systems-manager/',
'session-manager-plugin-not-found'
)


class EcsExecuteCommand(ServiceOperation):

def create_help_command(self):
help_command = super(EcsExecuteCommand, self).create_help_command()
# change the output shape because the command provides no output.
self._operation_model.output_shape = None
return help_command


class ExecuteCommandCaller(CLIOperationCaller):
def invoke(self, service_name, operation_name, parameters, parsed_globals):
try:
# making an execute-command call to connect to an active session on a container would require
# session-manager-plugin to be installed on the client machine.
# Hence, making this empty session-manager-plugin call before calling execute-command
# to ensure that session-manager-plugin is installed before execute-command-command is made
check_call(["session-manager-plugin"])
client = self._session.create_client(
service_name, region_name=parsed_globals.region,
endpoint_url=parsed_globals.endpoint_url,
verify=parsed_globals.verify_ssl)
response = client.execute_command(**parameters)
region_name = client.meta.region_name
# ignore_user_entered_signals ignores these signals
# because if signals which kills the process are not
# captured would kill the foreground process but not the
# background one. Capturing these would prevents process
# from getting killed and these signals are input to plugin
# and handling in there
with ignore_user_entered_signals():
# call executable with necessary input
check_call(["session-manager-plugin",
json.dumps(response['session']),
region_name,
"StartSession"])
return 0
except OSError as ex:
if ex.errno == errno.ENOENT:
logger.debug('SessionManagerPlugin is not present',
exc_info=True)
raise ValueError(''.join(ERROR_MESSAGE))
65 changes: 65 additions & 0 deletions tests/functional/ecs/test_execute_command.py
@@ -0,0 +1,65 @@
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# 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 mock
import errno
import json

from awscli.testutils import BaseAWSCommandParamsTest
from awscli.testutils import BaseAWSHelpOutputTest


class TestExecuteCommand(BaseAWSCommandParamsTest):

@mock.patch('awscli.customizations.ecs.executeCommand.check_call')
def test_execute_command_success(self, mock_check_call):
cmdline = 'ecs execute-command --cluster someCluster --task someTaskId ' \
'--interactive --command ls --region use-west-2'
mock_check_call.return_value = 0
self.parsed_responses = [{
"containerName": "someContainerName",
"containerArn" : "someContainerArn",
"taskArn": "someTaskArn",
"session": {"sessionId": "session-id", "tokenValue": "token-value", "streamUrl": "stream-url"},
"clusterArn": "someClusterArn",
"interactive": "true"
}]
self.run_cmd(cmdline, expected_rc=0)
self.assertEqual(self.operations_called[0][0].name,
'ExecuteCommand'
)
actual_response = json.loads(mock_check_call.call_args[0][0][1])
self.assertEqual(
{
"containerName": "someContainerName",
"containerArn" : "someContainerArn",
"taskArn": "someTaskArn",
"session": {"sessionId": "session-id", "tokenValue": "token-value", "streamUrl": "stream-url"},
"clusterArn": "someClusterArn",
"interactive": "true"
},
actual_response
)

@mock.patch('awscli.customizations.ecs.executeCommand.check_call')
def test_execute_command_fails(self, mock_check_call):
cmdline = 'ecs execute-command --cluster someCluster --task someTaskId ' \
'--interactive --command ls --region use-west-2'
mock_check_call.side_effect = OSError(errno.ENOENT, 'some error')
self.run_cmd(cmdline, expected_rc=255)


class TestHelpOutput(BaseAWSHelpOutputTest):

def test_execute_command_output(self):
self.driver.main(['ecs', 'execute-command', 'help'])
self.assert_contains('Output\n======\n\nNone')
116 changes: 116 additions & 0 deletions tests/unit/customizations/ecs/test_executecommand_startsession.py
@@ -0,0 +1,116 @@
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# 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 mock
import botocore.session
import json
import errno

import unittest
from awscli.customizations.ecs import executeCommand


class TestExecuteCommand(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.caller = executeCommand.ExecuteCommandCaller(self.session)
self.session.create_client.return_value = self.client

@mock.patch('awscli.customizations.ecs.executeCommand.check_call')
def test_execute_command_when_calls_fails_from_ecs(self, mock_check_call):
self.client.execute_command.side_effect = Exception('some exception')
mock_check_call.return_value = 0
with self.assertRaisesRegexp(Exception, 'some exception'):
self.caller.invoke('ecs', 'ExecuteCommand', {}, mock.Mock())

@mock.patch('awscli.customizations.ecs.executeCommand.check_call')
def test_execute_command_session_manager_plugin_not_installed_scenario(self, mock_check_call):
mock_check_call.side_effect = [OSError(errno.ENOENT, 'some error'), 0]

with self.assertRaises(ValueError):
self.caller.invoke('ecs', 'ExecuteCommand', {}, mock.Mock())

@mock.patch('awscli.customizations.ecs.executeCommand.check_call')
def test_execute_command_success_scenario(self, mock_check_call):
mock_check_call.return_value = 0
execute_command_params = {
"cluster": "default",
"task": "someTaskId",
"command": "ls",
"interactive": "true"
}

execute_command_response = {
"containerName": "someContainerName",
"containerArn": "someContainerArn",
"taskArn": "someTaskArn",
"session": {"sessionId": "session-id", "tokenValue": "token-value", "streamUrl": "stream-url"},
"clusterArn": "someClusterArn",
"interactive": "true"
}

self.client.execute_command.return_value = execute_command_response

rc = self.caller.invoke('ecs', 'ExecuteCommand', execute_command_params, mock.Mock())

self.assertEquals(rc, 0)
self.client.execute_command.assert_called_with(**execute_command_params)

mock_check_call_list = mock_check_call.call_args[0][0]
mock_check_call_list[1] = json.loads(mock_check_call_list[1])
self.assertEqual(
mock_check_call_list,
['session-manager-plugin',
execute_command_response["session"],
self.region,
'StartSession']
)

@mock.patch('awscli.customizations.ecs.executeCommand.check_call')
def test_execute_command_when_check_call_fails(self, mock_check_call):
mock_check_call.side_effect = [0, Exception('some Exception')]

execute_command_params = {
"cluster": "default",
"task": "someTaskId",
"command": "ls",
"interactive": "true"
}

execute_command_response = {
"containerName": "someContainerName",
"containerArn": "someContainerArn",
"taskArn": "someTaskArn",
"session": {"sessionId": "session-id", "tokenValue": "token-value", "streamUrl": "stream-url"},
"clusterArn": "someClusterArn",
"interactive": "true"
}

self.client.execute_command.return_value = execute_command_response

with self.assertRaises(Exception):
self.caller.invoke('ecs', 'ExecuteCommand', execute_command_params, mock.Mock())

mock_check_call_list = mock_check_call.call_args[0][0]
mock_check_call_list[1] = json.loads(mock_check_call_list[1])
self.assertEqual(
mock_check_call_list,
['session-manager-plugin',
execute_command_response["session"],
self.region,
'StartSession'])

0 comments on commit ba4163d

Please sign in to comment.