From f3188fcf10565b04ef2ab9c666bf3592d4ab9d9b Mon Sep 17 00:00:00 2001 From: One-Fine-Day Date: Wed, 27 Dec 2017 13:59:55 -0600 Subject: [PATCH] Shipyard CLI for Configdocs Status Shipyard API to discover buffered and committed collections of configdocs 3 of 4 Commits (API, API Client, Documentation are in separate commits) CLI -added action to the configdocs status in get/actions.py -modified get configdocs command to accept the command without the collection in get/commands.py -added table formating for the collections in cli_format_common.py -unit tests are located in test_get_actions.py and test_get_commands.py Change-Id: I29ad088c1dd0bdaccd8c5328abb610a1c32f99aa --- shipyard_client/cli/cli_format_common.py | 84 +++++---- shipyard_client/cli/get/actions.py | 31 ++++ shipyard_client/cli/get/commands.py | 31 ++-- .../tests/unit/cli/get/test_get_actions.py | 168 ++++++++++++------ .../tests/unit/cli/get/test_get_commands.py | 25 +-- 5 files changed, 226 insertions(+), 113 deletions(-) diff --git a/shipyard_client/cli/cli_format_common.py b/shipyard_client/cli/cli_format_common.py index e28db1f4..ce88b10f 100644 --- a/shipyard_client/cli/cli_format_common.py +++ b/shipyard_client/cli/cli_format_common.py @@ -24,16 +24,14 @@ def gen_action_steps(step_list, action_id): Returns a string representation of the table. """ # Generate the steps table. - steps = format_utils.table_factory( - field_names=['Steps', 'Index', 'State'] - ) + steps = format_utils.table_factory(field_names=['Steps', 'Index', 'State']) if step_list: for step in step_list: - steps.add_row( - ['step/{}/{}'.format(action_id, step.get('id')), - step.get('index'), - step.get('state')] - ) + steps.add_row([ + 'step/{}/{}'.format(action_id, step.get('id')), + step.get('index'), + step.get('state') + ]) else: steps.add_row(['None', '', '']) @@ -47,13 +45,13 @@ def gen_action_commands(command_list): 'datetime'. """ cmds = format_utils.table_factory( - field_names=['Commands', 'User', 'Datetime'] - ) + field_names=['Commands', 'User', 'Datetime']) if command_list: for cmd in command_list: cmds.add_row( - [cmd.get('command'), cmd.get('user'), cmd.get('datetime')] - ) + [cmd.get('command'), + cmd.get('user'), + cmd.get('datetime')]) else: cmds.add_row(['None', '', '']) @@ -70,10 +68,8 @@ def gen_action_validations(validation_list): validations = [] for val in validation_list: validations.append('{} : validation/{}/{}\n'.format( - val.get('validation_name'), - val.get('action_id'), - val.get('id') - )) + val.get('validation_name'), val.get('action_id'), val.get( + 'id'))) validations.append(val.get('details')) validations.append('\n\n') return 'Validations: {}'.format('\n'.join(validations)) @@ -107,10 +103,8 @@ def gen_action_step_details(step_dict, action_id): """ details = format_utils.table_factory() details.add_row(['Name:', step_dict.get('task_id')]) - details.add_row(['Task ID:', 'step/{}/{}'.format( - action_id, - step_dict.get('task_id') - )]) + details.add_row( + ['Task ID:', 'step/{}/{}'.format(action_id, step_dict.get('task_id'))]) details.add_row(['Index:', step_dict.get('index')]) details.add_row(['State:', step_dict.get('state')]) details.add_row(['Start Date:', step_dict.get('start_date')]) @@ -128,34 +122,53 @@ def gen_action_table(action_list): 'action_lifecycle' """ actions = format_utils.table_factory( - field_names=['Name', 'Action', 'Lifecycle'] - ) + field_names=['Name', 'Action', 'Lifecycle']) if action_list: for action in action_list: - actions.add_row( - [action.get('name'), - 'action/{}'.format(action.get('id')), - action.get('action_lifecycle')] - ) + actions.add_row([ + action.get('name'), 'action/{}'.format(action.get('id')), + action.get('action_lifecycle') + ]) else: actions.add_row(['None', '', '']) return format_utils.table_get_string(actions) +def gen_collection_table(collection_list): + """Generates a list of collections and their status + + Assumes collection_list is a list of dictionares with 'collection_name', + 'committed_status', and 'buffer_status' + """ + collections = format_utils.table_factory( + field_names=['Collection', 'Committed', 'Buffer']) + + if collection_list: + for collection in collection_list: + collections.add_row([ + collection.get('collection_name'), + collection.get('committed_status'), + collection.get('buffer_status') + ]) + else: + collections.add_row(['None', '', '']) + + return format_utils.table_get_string(collections) + + def gen_workflow_table(workflow_list): """Generates a list of workflows Assumes workflow_list is a list of dictionaries with 'workflow_id' and 'state' """ - workflows = format_utils.table_factory( - field_names=['Workflows', 'State'] - ) + workflows = format_utils.table_factory(field_names=['Workflows', 'State']) if workflow_list: for workflow in workflow_list: workflows.add_row( - [workflow.get('workflow_id'), workflow.get('state')]) + [workflow.get('workflow_id'), + workflow.get('state')]) else: workflows.add_row(['None', '']) @@ -177,8 +190,9 @@ def gen_workflow_details(workflow_dict): details.add_row(['Execution Date:', workflow_dict.get('execution_date')]) details.add_row(['Start Date:', workflow_dict.get('start_date')]) details.add_row(['End Date:', workflow_dict.get('end_date')]) - details.add_row(['External Trigger:', - workflow_dict.get('external_trigger')]) + details.add_row( + ['External Trigger:', + workflow_dict.get('external_trigger')]) return format_utils.table_get_string(details) @@ -187,9 +201,7 @@ def gen_workflow_steps(step_list): Assumes step_list is a list of dictionaries with 'task_id' and 'state' """ - steps = format_utils.table_factory( - field_names=['Steps', 'State'] - ) + steps = format_utils.table_factory(field_names=['Steps', 'State']) if step_list: for step in step_list: steps.add_row([step.get('task_id'), step.get('state')]) diff --git a/shipyard_client/cli/get/actions.py b/shipyard_client/cli/get/actions.py index a2a3925d..53702eb3 100644 --- a/shipyard_client/cli/get/actions.py +++ b/shipyard_client/cli/get/actions.py @@ -80,6 +80,37 @@ class GetConfigdocs(CliAction): return format_utils.raw_format_response_handler(response) +class GetConfigdocsStatus(CliAction): + """Action to get the configdocs status""" + + def __init__(self, ctx): + """Sets parameters.""" + super().__init__(ctx) + self.logger.debug("GetConfigdocsStatus action initialized") + + def invoke(self): + """Calls API Client and formats response from API Client""" + self.logger.debug("Calling API Client get_configdocs_status") + return self.get_api_client().get_configdocs_status() + + # Handle 404 with default error handler for cli. + cli_handled_err_resp_codes = [404] + + # Handle 200 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [200] + + def cli_format_response_handler(self, response): + """CLI output handler + + :param response: a requests response object + :returns: a string representing a CLI appropriate response + Handles 200 responses + """ + resp_j = response.json() + coll_list = resp_j if resp_j else [] + return cli_format_common.gen_collection_table(coll_list) + + class GetRenderedConfigdocs(CliAction): """Action to Get Rendered Configdocs""" diff --git a/shipyard_client/cli/get/commands.py b/shipyard_client/cli/get/commands.py index f8912082..b08af140 100644 --- a/shipyard_client/cli/get/commands.py +++ b/shipyard_client/cli/get/commands.py @@ -18,6 +18,7 @@ import click from shipyard_client.cli.get.actions import GetActions from shipyard_client.cli.get.actions import GetConfigdocs +from shipyard_client.cli.get.actions import GetConfigdocsStatus from shipyard_client.cli.get.actions import GetRenderedConfigdocs from shipyard_client.cli.get.actions import GetWorkflows @@ -64,7 +65,7 @@ SHORT_DESC_CONFIGDOCS = ("Retrieve documents loaded into Shipyard, either " @get.command( name='configdocs', help=DESC_CONFIGDOCS, short_help=SHORT_DESC_CONFIGDOCS) -@click.argument('collection') +@click.argument('collection', nargs=-1, required=False) @click.option( '--committed', '-c', @@ -80,20 +81,22 @@ SHORT_DESC_CONFIGDOCS = ("Retrieve documents loaded into Shipyard, either " 'collection, this will return an empty response (default)') @click.pass_context def get_configdocs(ctx, collection, buffer, committed): + if collection: + if buffer and committed: + ctx.fail( + 'You must choose whether to retrive the committed OR from the ' + 'Shipyard Buffer with --committed or --buffer. ') - if buffer and committed: - ctx.fail( - 'You must choose whether to retrive the committed OR from the ' - 'Shipyard Buffer with --committed or --buffer. ') + if committed: + version = 'committed' - if (not buffer and not committed) or buffer: - version = 'buffer' + else: + version = 'buffer' - if committed: - version = 'committed' - - click.echo( - GetConfigdocs(ctx, collection, version).invoke_and_return_resp()) + click.echo( + GetConfigdocs(ctx, collection, version).invoke_and_return_resp()) + else: + click.echo(GetConfigdocsStatus(ctx).invoke_and_return_resp()) DESC_RENDEREDCONFIGDOCS = """ @@ -157,9 +160,7 @@ SHORT_DESC_WORKFLOWS = "Lists the workflows from airflow." @get.command( - name='workflows', - help=DESC_WORKFLOWS, - short_help=SHORT_DESC_WORKFLOWS) + name='workflows', help=DESC_WORKFLOWS, short_help=SHORT_DESC_WORKFLOWS) @click.option( '--since', help=('A boundary in the past within which to retrieve results.' diff --git a/shipyard_client/tests/unit/cli/get/test_get_actions.py b/shipyard_client/tests/unit/cli/get/test_get_actions.py index 44ba5a6f..a07be396 100644 --- a/shipyard_client/tests/unit/cli/get/test_get_actions.py +++ b/shipyard_client/tests/unit/cli/get/test_get_actions.py @@ -12,12 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. import mock +from mock import patch +import pytest import responses from shipyard_client.api_client.base_client import BaseClient +from shipyard_client.cli import cli_format_common from shipyard_client.cli.get.actions import GetActions from shipyard_client.cli.get.actions import GetConfigdocs +from shipyard_client.cli.get.actions import GetConfigdocsStatus from shipyard_client.cli.get.actions import GetRenderedConfigdocs from shipyard_client.cli.get.actions import GetWorkflows from shipyard_client.tests.unit.cli import stubs @@ -64,10 +68,11 @@ GET_ACTIONS_API_RESP = """ @mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') @mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') def test_get_actions(*args): - responses.add(responses.GET, - 'http://shiptest/actions', - body=GET_ACTIONS_API_RESP, - status=200) + responses.add( + responses.GET, + 'http://shiptest/actions', + body=GET_ACTIONS_API_RESP, + status=200) response = GetActions(stubs.StubCliContext()).invoke_and_return_resp() assert 'deploy_site' in response assert 'action/01BTP9T2WCE1PAJR2DWYXG805V' in response @@ -78,10 +83,8 @@ def test_get_actions(*args): @mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') @mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') def test_get_actions_empty(*args): - responses.add(responses.GET, - 'http://shiptest/actions', - body="[]", - status=200) + responses.add( + responses.GET, 'http://shiptest/actions', body="[]", status=200) response = GetActions(stubs.StubCliContext()).invoke_and_return_resp() assert 'None' in response assert 'Lifecycle' in response @@ -100,13 +103,14 @@ yaml2: yaml2 @mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') @mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') def test_get_configdocs(*args): - responses.add(responses.GET, - 'http://shiptest/configdocs/design?version=buffer', - body=GET_CONFIGDOCS_API_RESP, - status=200) - response = GetConfigdocs(stubs.StubCliContext(), - collection='design', - version='buffer').invoke_and_return_resp() + responses.add( + responses.GET, + 'http://shiptest/configdocs/design?version=buffer', + body=GET_CONFIGDOCS_API_RESP, + status=200) + response = GetConfigdocs( + stubs.StubCliContext(), collection='design', + version='buffer').invoke_and_return_resp() assert response == GET_CONFIGDOCS_API_RESP @@ -114,23 +118,82 @@ def test_get_configdocs(*args): @mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') @mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') def test_get_configdocs_not_found(*args): - api_resp = stubs.gen_err_resp(message='Not Found', - sub_error_count=0, - sub_info_count=0, - reason='It does not exist', - code=404) + api_resp = stubs.gen_err_resp( + message='Not Found', + sub_error_count=0, + sub_info_count=0, + reason='It does not exist', + code=404) - responses.add(responses.GET, - 'http://shiptest/configdocs/design?version=buffer', - body=api_resp, - status=404) - response = GetConfigdocs(stubs.StubCliContext(), - collection='design', - version='buffer').invoke_and_return_resp() + responses.add( + responses.GET, + 'http://shiptest/configdocs/design?version=buffer', + body=api_resp, + status=404) + response = GetConfigdocs( + stubs.StubCliContext(), collection='design', + version='buffer').invoke_and_return_resp() assert 'Error: Not Found' in response assert 'Reason: It does not exist' in response +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +@pytest.mark.parametrize("test_input, expected", [(""" +[ + { + "collection_name":"Collection_1", + "committed_status": "present", + "buffer_status": "unmodified" + }, + { + "collection_name":"Collection_2", + "committed_status": "present", + "buffer_status": "modified" + }, + { + "collection_name":"Collection_3", + "committed_status": "not present", + "buffer_status": "created" + }, + { + "collection_name":"Collection_A", + "committed_status": "present", + "buffer_status": "deleted" + } +] +""", [{ + "collection_name": "Collection_1", + "committed_status": "present", + "buffer_status": "unmodified" +}, { + "collection_name": "Collection_2", + "committed_status": "present", + "buffer_status": "modified" +}, { + "collection_name": "Collection_3", + "committed_status": "not present", + "buffer_status": "created" +}, { + "collection_name": "Collection_A", + "committed_status": "present", + "buffer_status": "deleted" +}])]) +def test_get_configdocs_status(test_input, expected, *args): + responses.add( + responses.GET, + 'http://shiptest/configdocs', + body=test_input, + status=200) + + with patch.object(cli_format_common, + 'gen_collection_table') as mock_method: + response = GetConfigdocsStatus( + stubs.StubCliContext()).invoke_and_return_resp() + mock_method.assert_called_once_with(expected) + + GET_RENDEREDCONFIGDOCS_API_RESP = """ --- yaml: yaml @@ -144,13 +207,13 @@ yaml2: yaml2 @mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') @mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') def test_get_renderedconfigdocs(*args): - responses.add(responses.GET, - 'http://shiptest/renderedconfigdocs?version=buffer', - body=GET_RENDEREDCONFIGDOCS_API_RESP, - status=200) + responses.add( + responses.GET, + 'http://shiptest/renderedconfigdocs?version=buffer', + body=GET_RENDEREDCONFIGDOCS_API_RESP, + status=200) response = GetRenderedConfigdocs( - stubs.StubCliContext(), - version='buffer').invoke_and_return_resp() + stubs.StubCliContext(), version='buffer').invoke_and_return_resp() assert response == GET_RENDEREDCONFIGDOCS_API_RESP @@ -158,18 +221,20 @@ def test_get_renderedconfigdocs(*args): @mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') @mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') def test_get_renderedconfigdocs_not_found(*args): - api_resp = stubs.gen_err_resp(message='Not Found', - sub_error_count=0, - sub_info_count=0, - reason='It does not exist', - code=404) + api_resp = stubs.gen_err_resp( + message='Not Found', + sub_error_count=0, + sub_info_count=0, + reason='It does not exist', + code=404) - responses.add(responses.GET, - 'http://shiptest/renderedconfigdocs?version=buffer', - body=api_resp, - status=404) - response = GetRenderedConfigdocs(stubs.StubCliContext(), - version='buffer').invoke_and_return_resp() + responses.add( + responses.GET, + 'http://shiptest/renderedconfigdocs?version=buffer', + body=api_resp, + status=404) + response = GetRenderedConfigdocs( + stubs.StubCliContext(), version='buffer').invoke_and_return_resp() assert 'Error: Not Found' in response assert 'Reason: It does not exist' in response @@ -204,10 +269,11 @@ GET_WORKFLOWS_API_RESP = """ @mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') @mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') def test_get_workflows(*args): - responses.add(responses.GET, - 'http://shiptest/workflows', - body=GET_WORKFLOWS_API_RESP, - status=200) + responses.add( + responses.GET, + 'http://shiptest/workflows', + body=GET_WORKFLOWS_API_RESP, + status=200) response = GetWorkflows(stubs.StubCliContext()).invoke_and_return_resp() assert 'deploy_site__2017-10-09T21:19:03.000000' in response assert 'deploy_site__2017-10-09T21:18:56.000000' in response @@ -219,10 +285,8 @@ def test_get_workflows(*args): @mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') @mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') def test_get_workflows_empty(*args): - responses.add(responses.GET, - 'http://shiptest/workflows', - body="[]", - status=200) + responses.add( + responses.GET, 'http://shiptest/workflows', body="[]", status=200) response = GetWorkflows(stubs.StubCliContext()).invoke_and_return_resp() assert 'None' in response assert 'State' in response diff --git a/shipyard_client/tests/unit/cli/get/test_get_commands.py b/shipyard_client/tests/unit/cli/get/test_get_commands.py index 771e07f5..e10c91dc 100644 --- a/shipyard_client/tests/unit/cli/get/test_get_commands.py +++ b/shipyard_client/tests/unit/cli/get/test_get_commands.py @@ -16,11 +16,8 @@ from click.testing import CliRunner from mock import patch, ANY from shipyard_client.cli.get.actions import ( - GetActions, - GetConfigdocs, - GetRenderedConfigdocs, - GetWorkflows -) + GetActions, GetConfigdocs, GetConfigdocsStatus, GetRenderedConfigdocs, + GetWorkflows) from shipyard_client.cli.commands import shipyard auth_vars = ('--os-project-domain-name=OS_PROJECT_DOMAIN_NAME_test ' @@ -53,14 +50,23 @@ def test_get_actions_negative(*args): assert 'Error' in results.output -def test_get_configdocs(*args): +def test_get_configdocs_with_passing_collection(*args): """test get_configdocs""" collection = 'design' runner = CliRunner() + # verify GetConfigdocs is called when a collection is entered with patch.object(GetConfigdocs, '__init__') as mock_method: runner.invoke(shipyard, [auth_vars, 'get', 'configdocs', collection]) - mock_method.assert_called_once_with(ANY, collection, 'buffer') + mock_method.assert_called_once_with(ANY, (collection, ), 'buffer') + + +def test_get_configdocs_without_passing_collection(*args): + # verify GetConfigdocsStatus is called when no arguments are entered + runner = CliRunner() + with patch.object(GetConfigdocsStatus, '__init__') as mock_method: + runner.invoke(shipyard, [auth_vars, 'get', 'configdocs']) + mock_method.assert_called_once_with(ANY) def test_get_configdocs_negative(*args): @@ -79,7 +85,6 @@ def test_get_configdocs_negative(*args): def test_get_renderedconfigdocs(*args): """test get_rendereddocs""" - runner = CliRunner() with patch.object(GetRenderedConfigdocs, '__init__') as mock_method: runner.invoke(shipyard, [auth_vars, 'get', 'renderedconfigdocs']) @@ -119,6 +124,6 @@ def test_get_workflows_negative(*args): invalid_arg = 'invalid_date' runner = CliRunner() - results = runner.invoke( - shipyard, [auth_vars, 'get', 'workflows', invalid_arg]) + results = runner.invoke(shipyard, + [auth_vars, 'get', 'workflows', invalid_arg]) assert 'Error' in results.output