From ac7261d871f6dde64154f068bdb73bd2aabec46a Mon Sep 17 00:00:00 2001 From: Stuart Leeks Date: Fri, 21 Oct 2022 09:27:24 +0100 Subject: [PATCH] CLI Updates (#2747) * Add invoke action commands * Add shell completion for shared service IDs * Remove TODO comment * Add basic table query for overall costs * Add migrations command * Handle missing app role name * Add raw output option * Add airlock request title * airlock -> airlock-request and fix error text * Fix airlock request id shell completion * Add table format for airlock request submit response * Fix up airlock request review URL * Add example airlock-request creation * Add CLI changes to CHANGELOG * Add table format to airlock review * Fix table output for airlock list * Add airlock-request cancel command * Add TODO comments * Add workspace service delete command * Update comment on table formatting * Update output command to take response and exit on error * Update api_client typing to avoid private module import * Add type annotations to shell_complete methods --- CHANGELOG.md | 1 + api_app/_version.py | 2 +- api_app/services/aad_authentication.py | 2 +- cli/README.md | 34 ++++- cli/tre/api_client.py | 4 +- cli/tre/commands/costs.py | 10 +- cli/tre/commands/health.py | 2 +- cli/tre/commands/migrations.py | 24 ++++ cli/tre/commands/operation.py | 9 +- .../shared_service_template.py | 4 +- .../shared_service_templates.py | 4 +- cli/tre/commands/shared_services/operation.py | 2 +- .../shared_services/shared_service.py | 19 ++- .../shared_services/shared_services.py | 4 +- .../user_resource_template.py | 4 +- .../user_resource_templates.py | 4 +- .../workspace_service_template.py | 4 +- .../workspace_service_templates.py | 4 +- .../workspace_templates/workspace_template.py | 4 +- .../workspace_templates.py | 5 +- .../commands/workspaces/airlock/request.py | 66 +++++++--- .../commands/workspaces/airlock/requests.py | 12 +- cli/tre/commands/workspaces/workspace.py | 60 ++++++++- .../workspace_services/operation.py | 2 +- .../user_resources/operation.py | 2 +- .../user_resources/user_resource.py | 65 +++++++-- .../user_resources/user_resources.py | 4 +- .../workspace_services/workspace_service.py | 124 ++++++++++++++++-- .../workspace_services/workspace_services.py | 4 +- cli/tre/commands/workspaces/workspaces.py | 4 +- cli/tre/main.py | 3 +- cli/tre/output.py | 18 ++- 32 files changed, 420 insertions(+), 90 deletions(-) create mode 100644 cli/tre/commands/migrations.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b471fb6268..b46c83eb75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ ENHANCEMENTS: * Upgrade Github Actions versions ([#2731](https://github.com/microsoft/AzureTRE/pull/2744)) * Install TRE CLI inside the devcontainer image (rather than via a post-create step) ([#2757](https://github.com/microsoft/AzureTRE/pull/2757)) * Upgrade Terraform to 1.3.2 ([#2758](https://github.com/microsoft/AzureTRE/pull/2758)) +* `tre` CLI: added `raw` output option, improved `airlock-requests` handling, more consistent exit codes on error, added examples to CLI README.md BUG FIXES: diff --git a/api_app/_version.py b/api_app/_version.py index d3563f072c..92fced85ba 100644 --- a/api_app/_version.py +++ b/api_app/_version.py @@ -1 +1 @@ -__version__ = "0.4.54" +__version__ = "0.4.55" diff --git a/api_app/services/aad_authentication.py b/api_app/services/aad_authentication.py index 40640bcfff..2c1a36b7a6 100644 --- a/api_app/services/aad_authentication.py +++ b/api_app/services/aad_authentication.py @@ -265,7 +265,7 @@ def get_workspace_role_assignment_details(self, workspace: Workspace): if principal_type == "User" and principal_id in user_emails: app_role_id = role_assignment["appRoleId"] - app_role_name = inverted_app_role_ids[app_role_id] + app_role_name = inverted_app_role_ids.get(app_role_id) if app_role_name: workspace_role_assignments_details[app_role_name].append(user_emails[principal_id]) diff --git a/cli/README.md b/cli/README.md index 9271518565..f1d8e015ab 100644 --- a/cli/README.md +++ b/cli/README.md @@ -144,7 +144,15 @@ The commands corresponding to these asynchronous operations will poll this resul ### Output formats -Most commands support formatting output as `json` (default), `table`, or `none` via the `--output` option. This can also be controlled using the `TRECLI_OUTPUT` environment variable, i.e. set `TRECLI_OUTPUT` to `table` to default to the table output format. +Most commands support formatting output as `table` (default), `json`, `jsonc`, `raw`, or `none` via the `--output` option. This can also be controlled using the `TRECLI_OUTPUT` environment variable, i.e. set `TRECLI_OUTPUT` to `table` to default to the table output format. + +| Option | Description | +| ------- | ----------------------------------------------------------------------------- | +| `table` | Works well for interactive use | +| `json` | Plain JSON output, ideal for parsing via `jq` or other tools | +| `jsonc` | Coloured, formatted JSON | +| `raw` | Results are output as-is. Useful with `--query` when capturing a single value | +| `none` | No output | ### Querying output @@ -211,3 +219,27 @@ Or you can load the content from a file that contains embedded environment varia When you run `tre login` you specify the base URL for the API, but when you are developing AzureTRE you may want to make calls against the locally running API. To support this, you can set the `TRECLI_BASE_URL` environment variable and that will override the API endpoint used by the CLI. + + +## Example usage + +### Creating an import airlock request + +```bash +# Set the ID of the workspace to create the import request for +WORKSPACE_ID=__ADD_ID_HERE__ + +# Create the airlock request - change the justification as appropriate +request=$(tre workspace $WORKSPACE_ID airlock-requests new --type import --title "Ant" --justification "It's import-ant" --output json) +request_id=$(echo $request | jq -r .airlockRequest.id) + +# Get the storage upload URL +upload_url=$(tre workspace $WORKSPACE_ID airlock-request $request_id get-url --query containerUrl --output raw) + +# Use the az CLI to upload ant.txt from the current directory (change as required) +az storage blob upload-batch --source . --pattern ant.txt --destination $upload_url + +# Submit the request for review +tre workspace $WORKSPACE_ID airlock-request $request_id submit + +``` diff --git a/cli/tre/api_client.py b/cli/tre/api_client.py index c60c4261c6..1d6cff2fc4 100644 --- a/cli/tre/api_client.py +++ b/cli/tre/api_client.py @@ -1,4 +1,5 @@ import sys +from typing import Union import click import json import msal @@ -80,11 +81,12 @@ def call_api( json_data=None, scope_id: str = None, throw_on_error: bool = True, + params: "Union[dict[str, str], None]" = None ) -> Response: with Client(verify=self.verify) as client: headers = headers.copy() headers['Authorization'] = f"Bearer {self.get_auth_token(log, scope_id)}" - response = client.request(method, f'{self.base_url}{url}', headers=headers, json=json_data) + response = client.request(method, f'{self.base_url}{url}', headers=headers, json=json_data, params=params) if throw_on_error and response.is_error: error_info = { 'status_code': response.status_code, diff --git a/cli/tre/commands/costs.py b/cli/tre/commands/costs.py index c954e22265..fa647307f4 100644 --- a/cli/tre/commands/costs.py +++ b/cli/tre/commands/costs.py @@ -41,10 +41,12 @@ def costs_overall(from_date, to_date, granularity, output_format, query): url = url + "?" + query_string response = client.call_api(log, 'GET', url) - # TODO - default table format output( - response.text, + response, output_format=output_format, + # To properly flatten the costs structure for table rendering, we need `let` + # as per https://jmespath.site/#wiki-lexical-scopes. For now: + default_table_query="[{category: 'core_services', id: null, name: null, costs: core_services}, shared_services[].{category:'shared_service', id:id, name:name, costs:costs}, workspaces[].{category:'workspace', id:id, name:name, costs:costs}] | []", query=query) return response.text @@ -80,9 +82,9 @@ def workspace_costs(workspace_id, from_date, to_date, granularity, output_format url = url + "?" + query_string response = client.call_api(log, 'GET', url) - # TODO - default table format + # TODO - default table format (needs JMESPath let, as per https://jmespath.site/#wiki-lexical-scopes) output( - response.text, + response, output_format=output_format, query=query) return response.text diff --git a/cli/tre/commands/health.py b/cli/tre/commands/health.py index 96f941b5e7..4b2e7b96c9 100644 --- a/cli/tre/commands/health.py +++ b/cli/tre/commands/health.py @@ -14,7 +14,7 @@ def health(output_format, query) -> None: client = ApiClient.get_api_client_from_config() response = client.call_api(log, 'GET', '/api/health') output( - response.text, + response, output_format=output_format, query=query, default_table_query="services") diff --git a/cli/tre/commands/migrations.py b/cli/tre/commands/migrations.py new file mode 100644 index 0000000000..2efde5f50e --- /dev/null +++ b/cli/tre/commands/migrations.py @@ -0,0 +1,24 @@ +import sys +import click +import logging + +from tre.api_client import ApiClient +from tre.output import output, output_option, query_option + + +@click.command(name="migrations", help="Run migrations") +@output_option() +@query_option() +def migrations(output_format, query) -> None: + log = logging.getLogger(__name__) + + client = ApiClient.get_api_client_from_config() + response = client.call_api(log, 'POST', '/api/migrations') + output( + response, + output_format=output_format, + query=query, + default_table_query="migrations") + + if not response.is_success: + sys.exit(1) diff --git a/cli/tre/commands/operation.py b/cli/tre/commands/operation.py index 535d33fd4c..222aef84f6 100644 --- a/cli/tre/commands/operation.py +++ b/cli/tre/commands/operation.py @@ -1,3 +1,4 @@ +from logging import Logger import sys from time import sleep @@ -6,7 +7,7 @@ from tre.output import output -def get_operation_id_completion(ctx, log, list_url, param, incomplete, scope_id: str = None): +def get_operation_id_completion(ctx: click.Context, log: Logger, list_url: str, param: click.Parameter, incomplete: str, scope_id: str = None): client = ApiClient.get_api_client_from_config() response = client.call_api(log, 'GET', list_url, scope_id=scope_id) if response.is_success: @@ -85,8 +86,8 @@ def operation_show(log, operation_url, no_wait, output_format, query, suppress_o action = response_json['operation']['action'] state = response_json['operation']['status'] - if not suppress_output: - output(response.text, output_format=output_format, query=query, default_table_query=default_operation_table_query_single()) + if not suppress_output or not response.is_success: + output(response, output_format=output_format, query=query, default_table_query=default_operation_table_query_single()) if wait_for_completion and not is_operation_state_success(state): sys.exit(1) @@ -103,4 +104,4 @@ def operations_list(log, operations_url, output_format, query, scope_id: str = N operations_url, scope_id=scope_id ) - output(response.text, output_format=output_format, query=query, default_table_query=default_operation_table_query_list()) + output(response, output_format=output_format, query=query, default_table_query=default_operation_table_query_list()) diff --git a/cli/tre/commands/shared_service_templates/shared_service_template.py b/cli/tre/commands/shared_service_templates/shared_service_template.py index 7336c1c862..9a697e2530 100644 --- a/cli/tre/commands/shared_service_templates/shared_service_template.py +++ b/cli/tre/commands/shared_service_templates/shared_service_template.py @@ -6,7 +6,7 @@ from .contexts import SharedServiceTemplateContext, pass_shared_service_template_context -def template_name_completion(ctx, param, incomplete): +def template_name_completion(ctx: click.Context, param: click.Parameter, incomplete: str): log = logging.getLogger(__name__) client = ApiClient.get_api_client_from_config() response = client.call_api(log, 'GET', '/api/shared-service-templates') @@ -41,7 +41,7 @@ def shared_service_template_show(shared_service_template_context: SharedServiceT f'/api/shared-service-templates/{template_name}', ) - output(response.text, output_format=output_format, query=query, default_table_query=r"{id: id, name:name, title: title, version:version, description:description}") + output(response, output_format=output_format, query=query, default_table_query=r"{id: id, name:name, title: title, version:version, description:description}") shared_service_template.add_command(shared_service_template_show) diff --git a/cli/tre/commands/shared_service_templates/shared_service_templates.py b/cli/tre/commands/shared_service_templates/shared_service_templates.py index 3b6e8d85a7..1603bf12de 100644 --- a/cli/tre/commands/shared_service_templates/shared_service_templates.py +++ b/cli/tre/commands/shared_service_templates/shared_service_templates.py @@ -23,7 +23,9 @@ def shared_service_templates_list(output_format, query): 'GET', '/api/shared-service-templates', ) - output(response.text, output_format=output_format, query=query, default_table_query=r"templates[].{name:name, title: title, description:description}") + output(response, output_format=output_format, query=query, default_table_query=r"templates[].{name:name, title: title, description:description}") shared_service_templates.add_command(shared_service_templates_list) + +# TODO register shared service template diff --git a/cli/tre/commands/shared_services/operation.py b/cli/tre/commands/shared_services/operation.py index c715695168..836c900e4a 100644 --- a/cli/tre/commands/shared_services/operation.py +++ b/cli/tre/commands/shared_services/operation.py @@ -7,7 +7,7 @@ from .contexts import pass_shared_service_operation_context, SharedServiceOperationContext -def operation_id_completion(ctx, param, incomplete): +def operation_id_completion(ctx: click.Context, param: click.Parameter, incomplete: str): log = logging.getLogger(__name__) parent_ctx = ctx.parent workspace_id = parent_ctx.params["workspace_id"] diff --git a/cli/tre/commands/shared_services/shared_service.py b/cli/tre/commands/shared_services/shared_service.py index 2ddf08432e..0c2ef30aa7 100644 --- a/cli/tre/commands/shared_services/shared_service.py +++ b/cli/tre/commands/shared_services/shared_service.py @@ -10,8 +10,17 @@ from .operations import shared_service_operations +def shared_service_id_completion(ctx: click.Context, param: click.Parameter, incomplete: str): + log = logging.getLogger(__name__) + client = ApiClient.get_api_client_from_config() + response = client.call_api(log, 'GET', '/api/shared-services') + if response.is_success: + ids = [shared_service["id"] for shared_service in response.json()["sharedServices"]] + return [id for id in ids if id.startswith(incomplete)] + + @click.group(invoke_without_command=True, help="Perform actions on an individual shared_service") -@click.argument('shared_service_id', required=True, type=click.UUID) +@click.argument('shared_service_id', required=True, type=click.UUID, shell_complete=shared_service_id_completion) @click.pass_context def shared_service(ctx: click.Context, shared_service_id: str) -> None: ctx.obj = SharedServiceContext(shared_service_id) @@ -30,12 +39,10 @@ def shared_service_show(shared_service_context: SharedServiceContext, output_for client = ApiClient.get_api_client_from_config() response = client.call_api(log, 'GET', f'/api/shared-services/{shared_service_id}', ) - output(response.text, output_format=output_format, query=query, default_table_query=r"sharedServices[].{id:id,name:templateName, version:templateVersion, is_enabled:isEnabled, status: deploymentStatus}") + output(response, output_format=output_format, query=query, default_table_query=r"sharedServices[].{id:id,name:templateName, version:templateVersion, is_enabled:isEnabled, status: deploymentStatus}") # TODO - add PATCH (and ?set-enabled) -# TODO - invoke action - @click.command(name="invoke-action", help="Invoke an action on a shared_service") @click.argument('action-name', required=True) @@ -61,7 +68,7 @@ def shared_service_invoke_action(shared_service_context: SharedServiceContext, c f'/api/shared-services/{shared_service_id}/invoke-action?action={action_name}' ) if no_wait: - output(response.text, output_format=output_format, query=query) + output(response, output_format=output_format, query=query) else: operation_url = response.headers['location'] operation_show(log, operation_url, no_wait=False, output_format=output_format, query=query) @@ -90,7 +97,7 @@ def shared_service_delete(shared_service_context: SharedServiceContext, ctx: cli click.echo("Deleting shared_service...\n", err=True) response = client.call_api(log, 'DELETE', f'/api/shared-services/{shared_service_id}') if no_wait: - output(response.text, output_format=output_format, query=query) + output(response, output_format=output_format, query=query) else: operation_url = response.headers['location'] operation_show(log, operation_url, no_wait=False, output_format=output_format, query=query) diff --git a/cli/tre/commands/shared_services/shared_services.py b/cli/tre/commands/shared_services/shared_services.py index a4331bdb16..85dcea3ad1 100644 --- a/cli/tre/commands/shared_services/shared_services.py +++ b/cli/tre/commands/shared_services/shared_services.py @@ -20,7 +20,7 @@ def shared_services_list(output_format, query): client = ApiClient.get_api_client_from_config() response = client.call_api(log, 'GET', '/api/shared-services') - output(response.text, output_format=output_format, query=query, default_table_query=r"sharedServices[].{id:id,name:templateName, version:templateVersion, is_enabled:isEnabled, status: deploymentStatus}") + output(response, output_format=output_format, query=query, default_table_query=r"sharedServices[].{id:id,name:templateName, version:templateVersion, is_enabled:isEnabled, status: deploymentStatus}") @click.command(name="new", help="Create a new shared_service") @@ -47,7 +47,7 @@ def shared_services_create(ctx, definition, definition_file, no_wait, output_for response = client.call_api(log, 'POST', '/api/shared-services', json_data=definition_dict) if no_wait: - output(response.text, output_format=output_format, query=query, default_table_query=default_operation_table_query_single()) + output(response, output_format=output_format, query=query, default_table_query=default_operation_table_query_single()) else: operation_url = response.headers['location'] operation_show(log, operation_url, no_wait=False, output_format=output_format, query=query) diff --git a/cli/tre/commands/workspace_service_templates/user_resource_templates/user_resource_template.py b/cli/tre/commands/workspace_service_templates/user_resource_templates/user_resource_template.py index e081cd8ae7..9c6e291a12 100644 --- a/cli/tre/commands/workspace_service_templates/user_resource_templates/user_resource_template.py +++ b/cli/tre/commands/workspace_service_templates/user_resource_templates/user_resource_template.py @@ -6,7 +6,7 @@ from .contexts import UserResourceTemplateContext, pass_user_resource_template_context -def template_name_completion(ctx, param, incomplete): +def template_name_completion(ctx: click.Context, param: click.Parameter, incomplete: str): log = logging.getLogger(__name__) parent_ctx = ctx.parent workspace_service_name = parent_ctx.params["template_name"] @@ -46,7 +46,7 @@ def user_resource_template_show(user_resource_template_context: UserResourceTemp f'/api/workspace-service-templates/{workspace_service_name}/user-resource-templates/{template_name}', ) - output(response.text, output_format=output_format, query=query, default_table_query=r"{id: id, name:name, title: title, version:version, description:description}") + output(response, output_format=output_format, query=query, default_table_query=r"{id: id, name:name, title: title, version:version, description:description}") user_resource_template.add_command(user_resource_template_show) diff --git a/cli/tre/commands/workspace_service_templates/user_resource_templates/user_resource_templates.py b/cli/tre/commands/workspace_service_templates/user_resource_templates/user_resource_templates.py index 643e33602c..eb3b82f4d5 100644 --- a/cli/tre/commands/workspace_service_templates/user_resource_templates/user_resource_templates.py +++ b/cli/tre/commands/workspace_service_templates/user_resource_templates/user_resource_templates.py @@ -30,7 +30,9 @@ def user_resource_templates_list(workspace_service_template_context: WorkspaceSe 'GET', f'/api/workspace-service-templates/{template_name}/user-resource-templates', ) - output(response.text, output_format=output_format, query=query, default_table_query=r"templates[].{name:name, title: title, description:description}") + output(response, output_format=output_format, query=query, default_table_query=r"templates[].{name:name, title: title, description:description}") user_resource_templates.add_command(user_resource_templates_list) + +# TODO register user resource template diff --git a/cli/tre/commands/workspace_service_templates/workspace_service_template.py b/cli/tre/commands/workspace_service_templates/workspace_service_template.py index 10234adce6..f6e3ad0923 100644 --- a/cli/tre/commands/workspace_service_templates/workspace_service_template.py +++ b/cli/tre/commands/workspace_service_templates/workspace_service_template.py @@ -9,7 +9,7 @@ from .user_resource_templates.user_resource_template import user_resource_template -def template_name_completion(ctx, param, incomplete): +def template_name_completion(ctx: click.Context, param: click.Parameter, incomplete: str): log = logging.getLogger(__name__) client = ApiClient.get_api_client_from_config() response = client.call_api(log, 'GET', '/api/workspace-service-templates') @@ -44,7 +44,7 @@ def workspace_service_template_show(workspace_service_template_context: Workspac f'/api/workspace-service-templates/{template_name}', ) - output(response.text, output_format=output_format, query=query, default_table_query=r"{id: id, name:name, title: title, version:version, description:description}") + output(response, output_format=output_format, query=query, default_table_query=r"{id: id, name:name, title: title, version:version, description:description}") workspace_service_template.add_command(workspace_service_template_show) diff --git a/cli/tre/commands/workspace_service_templates/workspace_service_templates.py b/cli/tre/commands/workspace_service_templates/workspace_service_templates.py index ad7068f074..f8a61c4f86 100644 --- a/cli/tre/commands/workspace_service_templates/workspace_service_templates.py +++ b/cli/tre/commands/workspace_service_templates/workspace_service_templates.py @@ -23,7 +23,9 @@ def workspace_service_templates_list(output_format, query): 'GET', '/api/workspace-service-templates', ) - output(response.text, output_format=output_format, query=query, default_table_query=r"templates[].{name:name, title: title, description:description}") + output(response, output_format=output_format, query=query, default_table_query=r"templates[].{name:name, title: title, description:description}") workspace_service_templates.add_command(workspace_service_templates_list) + +# TODO register workspace service template diff --git a/cli/tre/commands/workspace_templates/workspace_template.py b/cli/tre/commands/workspace_templates/workspace_template.py index aac0837769..8b2d838413 100644 --- a/cli/tre/commands/workspace_templates/workspace_template.py +++ b/cli/tre/commands/workspace_templates/workspace_template.py @@ -6,7 +6,7 @@ from .contexts import WorkspaceTemplateContext, pass_workspace_template_context -def template_name_completion(ctx, param, incomplete): +def template_name_completion(ctx: click.Context, param: click.Parameter, incomplete: str): log = logging.getLogger(__name__) client = ApiClient.get_api_client_from_config() response = client.call_api(log, 'GET', '/api/workspace-templates') @@ -41,7 +41,7 @@ def workspace_template_show(workspace_template_context: WorkspaceTemplateContext f'/api/workspace-templates/{template_name}', ) - output(response.text, output_format=output_format, query=query, default_table_query=r"{id: id, name:name, title: title, version:version, description:description}") + output(response, output_format=output_format, query=query, default_table_query=r"{id: id, name:name, title: title, version:version, description:description}") workspace_template.add_command(workspace_template_show) diff --git a/cli/tre/commands/workspace_templates/workspace_templates.py b/cli/tre/commands/workspace_templates/workspace_templates.py index 5670d96821..8e8d5a6e73 100644 --- a/cli/tre/commands/workspace_templates/workspace_templates.py +++ b/cli/tre/commands/workspace_templates/workspace_templates.py @@ -23,7 +23,10 @@ def workspace_templates_list(output_format, query): 'GET', '/api/workspace-templates', ) - output(response.text, output_format=output_format, query=query, default_table_query=r"templates[].{name:name, title: title, description:description}") + output(response, output_format=output_format, query=query, default_table_query=r"templates[].{name:name, title: title, description:description}") workspace_templates.add_command(workspace_templates_list) + + +# TODO register workspace template diff --git a/cli/tre/commands/workspaces/airlock/request.py b/cli/tre/commands/workspaces/airlock/request.py index 76abf32a02..a2ec060b69 100644 --- a/cli/tre/commands/workspaces/airlock/request.py +++ b/cli/tre/commands/workspaces/airlock/request.py @@ -5,10 +5,10 @@ from tre.commands.workspaces.airlock.contexts import WorkspaceAirlockContext, pass_workspace_airlock_context from tre.output import output, output_option, query_option -_default_table_query_item = r"airlockRequest.{id:id,workspace_id:workspaceId,type:requestType,status:status,business_justification:businessJustification}" +_default_table_query_item = r"airlockRequest.{id:id,workspace_id:workspaceId,type:requestType, title:requestTitle,status:status,business_justification:businessJustification}" -def airlock_id_completion(ctx: click.Context, param, incomplete): +def airlock_id_completion(ctx: click.Context, param: click.Parameter, incomplete: str): log = logging.getLogger(__name__) parent_ctx = ctx.parent workspace_id = parent_ctx.params["workspace_id"] @@ -16,11 +16,11 @@ def airlock_id_completion(ctx: click.Context, param, incomplete): workspace_scope = client.get_workspace_scope(log, workspace_id) response = client.call_api(log, 'GET', f'/api/workspaces/{workspace_id}/requests', scope_id=workspace_scope) if response.is_success: - ids = [workspace["id"] for workspace in response.json()["airlockRequests"]] + ids = [request["airlockRequest"]["id"] for request in response.json()["airlockRequests"]] return [id for id in ids if id.startswith(incomplete)] -@click.group(invoke_without_command=True, help="Perform actions on an airlock request") +@click.group(name="airlock-request", invoke_without_command=True, help="Perform actions on an airlock request") @click.argument('airlock_id', required=True, type=click.UUID, shell_complete=airlock_id_completion) @click.pass_context def airlock(ctx: click.Context, airlock_id: str) -> None: @@ -39,7 +39,7 @@ def airlock_show(airlock_context: WorkspaceAirlockContext, output_format, query) raise click.UsageError('Missing workspace ID') airlock_id = airlock_context.airlock_id if airlock_id is None: - raise click.UsageError('Missing service ID') + raise click.UsageError('Missing airlock request ID') client = ApiClient.get_api_client_from_config() workspace_scope = client.get_workspace_scope(log, workspace_id) @@ -51,7 +51,7 @@ def airlock_show(airlock_context: WorkspaceAirlockContext, output_format, query) scope_id=workspace_scope, ) - output(response.text, output_format=output_format, query=query, default_table_query=_default_table_query_item) + output(response, output_format=output_format, query=query, default_table_query=_default_table_query_item) @click.command(name="get-url", help="Get URL to access airlock request") @@ -78,10 +78,9 @@ def airlock_get_url(airlock_context: WorkspaceAirlockContext, output_format, que scope_id=workspace_scope, ) - output(response.text, output_format=output_format, query=query, default_table_query=r"{container_url:containerUrl}") + output(response, output_format=output_format, query=query, default_table_query=r"{container_url:containerUrl}") -# TODO table output default @click.command(name="submit", help="Submit an airlock request (after uploading content)") @output_option() @query_option() @@ -94,7 +93,7 @@ def airlock_submit(airlock_context: WorkspaceAirlockContext, output_format, quer raise click.UsageError('Missing workspace ID') airlock_id = airlock_context.airlock_id if airlock_id is None: - raise click.UsageError('Missing service ID') + raise click.UsageError('Missing airlock request ID') client = ApiClient.get_api_client_from_config() workspace_scope = client.get_workspace_scope(log, workspace_id) @@ -106,10 +105,13 @@ def airlock_submit(airlock_context: WorkspaceAirlockContext, output_format, quer scope_id=workspace_scope, ) - output(response.text, output_format=output_format, query=query) + output( + response, + output_format=output_format, + query=query, + default_table_query=_default_table_query_item) -# TODO table output default @click.command(name="review", help="Provide a review response for an airlock request") @click.option('--approve/--reject', 'approve', required=True, help="Approved/rejected") @click.option('--reason', required=True, help="Reason for approval/rejection") @@ -124,7 +126,7 @@ def airlock_review(airlock_context: WorkspaceAirlockContext, approve, reason, ou raise click.UsageError('Missing workspace ID') airlock_id = airlock_context.airlock_id if airlock_id is None: - raise click.UsageError('Missing service ID') + raise click.UsageError('Missing airlock request ID') client = ApiClient.get_api_client_from_config() workspace_scope = client.get_workspace_scope(log, workspace_id) @@ -132,7 +134,7 @@ def airlock_review(airlock_context: WorkspaceAirlockContext, approve, reason, ou response = client.call_api( log, 'POST', - f'/api/workspaces/{workspace_id}/requests/{airlock_id}/reviews', + f'/api/workspaces/{workspace_id}/requests/{airlock_id}/review', json_data={ "approval": approve, "decisionExplanation": reason, @@ -140,12 +142,46 @@ def airlock_review(airlock_context: WorkspaceAirlockContext, approve, reason, ou scope_id=workspace_scope, ) - output(response.text, output_format=output_format, query=query) + output( + response, + output_format=output_format, + query=query, + default_table_query=_default_table_query_item) + + +@click.command(name="cancel", help="Cancel an airlock request") +@output_option() +@query_option() +@pass_workspace_airlock_context +def airlock_cancel(airlock_context: WorkspaceAirlockContext, output_format, query) -> None: + log = logging.getLogger(__name__) + + workspace_id = airlock_context.workspace_id + if workspace_id is None: + raise click.UsageError('Missing workspace ID') + airlock_id = airlock_context.airlock_id + if airlock_id is None: + raise click.UsageError('Missing airlock request ID') + + client = ApiClient.get_api_client_from_config() + workspace_scope = client.get_workspace_scope(log, workspace_id) + + response = client.call_api( + log, + 'POST', + f'/api/workspaces/{workspace_id}/requests/{airlock_id}/cancel', + scope_id=workspace_scope, + ) + output( + response, + output_format=output_format, + query=query, + default_table_query=_default_table_query_item) -# TODO cancel airlock.add_command(airlock_show) airlock.add_command(airlock_get_url) airlock.add_command(airlock_submit) airlock.add_command(airlock_review) +airlock.add_command(airlock_cancel) diff --git a/cli/tre/commands/workspaces/airlock/requests.py b/cli/tre/commands/workspaces/airlock/requests.py index 3dfba265e9..599c3429aa 100644 --- a/cli/tre/commands/workspaces/airlock/requests.py +++ b/cli/tre/commands/workspaces/airlock/requests.py @@ -5,11 +5,11 @@ from tre.commands.workspaces.contexts import pass_workspace_context from tre.output import output, output_option, query_option -_default_table_query_list = r"airlockRequests[].{id:id,workspace_id:workspaceId,type:requestType,status:status,business_justification:businessJustification}" +_default_table_query_list = r"airlockRequests[].airlockRequest.{id:id,workspace_id:workspaceId,type:requestType,status:status,business_justification:businessJustification}" _default_table_query_item = r"airlockRequest.{id:id,workspace_id:workspaceId,type:requestType,status:status,business_justification:businessJustification}" -@click.group(help="List/add airlocks") +@click.group(name="airlock-requests", help="List/add airlock requests") def airlocks() -> None: pass @@ -34,16 +34,17 @@ def airlocks_list(workspace_context, output_format, query): f'/api/workspaces/{workspace_id}/requests', scope_id=workspace_scope, ) - output(response.text, output_format=output_format, query=query, default_table_query=_default_table_query_list) + output(response, output_format=output_format, query=query, default_table_query=_default_table_query_list) @click.command(name="new", help="Create a new airlock request") @click.option('--type', "request_type", help='The type of request', required=True, type=click.Choice(['import', 'export'])) +@click.option('--title', help='Title for the request', required=True) @click.option('--justification', help='Business justification for the request', required=True) @output_option() @query_option() @pass_workspace_context -def airlock_create(workspace_context, request_type, justification, output_format, query): +def airlock_create(workspace_context, request_type, title, justification, output_format, query): log = logging.getLogger(__name__) workspace_id = workspace_context.workspace_id @@ -60,11 +61,12 @@ def airlock_create(workspace_context, request_type, justification, output_format f'/api/workspaces/{workspace_id}/requests', json_data={ "requestType": request_type, + "requestTitle": title, "businessJustification": justification }, scope_id=workspace_scope) - output(response.text, output_format=output_format, query=query, default_table_query=_default_table_query_item) + output(response, output_format=output_format, query=query, default_table_query=_default_table_query_item) return response.text diff --git a/cli/tre/commands/workspaces/workspace.py b/cli/tre/commands/workspaces/workspace.py index d14efacddf..d8cea847d3 100644 --- a/cli/tre/commands/workspaces/workspace.py +++ b/cli/tre/commands/workspaces/workspace.py @@ -1,4 +1,5 @@ import json +import sys import click import logging @@ -15,7 +16,7 @@ from .airlock.request import airlock -def workspace_id_completion(ctx, param, incomplete): +def workspace_id_completion(ctx: click.Context, param: click.Parameter, incomplete: str): log = logging.getLogger(__name__) client = ApiClient.get_api_client_from_config() response = client.call_api(log, 'GET', '/api/workspaces') @@ -46,7 +47,7 @@ def workspace_show(workspace_context: WorkspaceContext, output_format, query): response = client.call_api(log, 'GET', f'/api/workspaces/{workspace_id}', ) output( - response.text, + response, output_format=output_format, query=query, default_table_query=r"workspace.{id:id, display_name:properties.display_name, deployment_status:deploymentStatus, workspace_url:workspaceURL}") @@ -89,7 +90,9 @@ def workspace_update(workspace_context: WorkspaceContext, ctx: click.Context, et json_data=definition_dict) if no_wait: - output(response.text, output_format=output_format, query=query, default_table_query=default_operation_table_query_single()) + output(response, output_format=output_format, query=query, default_table_query=default_operation_table_query_single()) + if not response.is_success: + sys.exit(1) else: operation_url = response.headers['location'] operation_show(log, operation_url, no_wait=False, output_format=output_format, query=query, suppress_output=suppress_output) @@ -123,8 +126,8 @@ def workspace_set_enabled(workspace_context: WorkspaceContext, etag, enable, no_ json_data={'isEnabled': enable}) if no_wait: - if not suppress_output: - output(response.text, output_format=output_format, query=query, default_table_query=default_operation_table_query_single()) + if not suppress_output or not response.is_success: + output(response, output_format=output_format, query=query, default_table_query=default_operation_table_query_single()) else: operation_url = response.headers['location'] operation_show(log, operation_url, no_wait=False, output_format=output_format, query=query, suppress_output=suppress_output) @@ -172,12 +175,55 @@ def workspace_delete(workspace_context: WorkspaceContext, ctx: click.Context, ye response = client.call_api(log, 'DELETE', f'/api/workspaces/{workspace_id}') if no_wait: - output(response.text, output_format=output_format, query=query, default_table_query=default_operation_table_query_single()) + output(response, output_format=output_format, query=query, default_table_query=default_operation_table_query_single()) + if not response.is_success: + sys.exit(1) else: operation_url = response.headers['location'] operation_show(log, operation_url, no_wait, output_format=output_format, query=query) -# TODO - invoke action + +@click.command(name="invoke-action", help="Invoke an action on a workspace") +@click.argument("action-name", required=True) +@click.option("--no-wait", flag_value=True, default=False) +@output_option() +@query_option() +@pass_workspace_context +def workspace_service_invoke_action( + workspace_context: WorkspaceContext, + action_name, + no_wait, + output_format, + query, +): + log = logging.getLogger(__name__) + + workspace_id = workspace_context.workspace_id + if workspace_id is None: + raise click.UsageError('Missing workspace ID') + + client = ApiClient.get_api_client_from_config() + + click.echo(f"Invoking action {action_name}...\n", err=True) + response = client.call_api( + log, + "POST", + f"/api/workspaces/{workspace_id}/invoke-action", + params={"action": action_name}, + ) + if no_wait: + output(response, output_format=output_format, query=query) + if not response.is_success: + sys.exit(1) + else: + operation_url = response.headers["location"] + operation_show( + log, + operation_url, + no_wait=False, + output_format=output_format, + query=query, + ) workspace.add_command(workspace_show) diff --git a/cli/tre/commands/workspaces/workspace_services/operation.py b/cli/tre/commands/workspaces/workspace_services/operation.py index 3ceb86fa44..5a50539b7a 100644 --- a/cli/tre/commands/workspaces/workspace_services/operation.py +++ b/cli/tre/commands/workspaces/workspace_services/operation.py @@ -7,7 +7,7 @@ from .contexts import pass_workspace_service_operation_context, WorkspaceServiceOperationContext -def operation_id_completion(ctx, param, incomplete): +def operation_id_completion(ctx: click.Context, param: click.Parameter, incomplete: str): log = logging.getLogger(__name__) parent_ctx = ctx.parent workspace_service_id = parent_ctx.params["workspace_service_id"] diff --git a/cli/tre/commands/workspaces/workspace_services/user_resources/operation.py b/cli/tre/commands/workspaces/workspace_services/user_resources/operation.py index 4fdee929e3..d9a44e4767 100644 --- a/cli/tre/commands/workspaces/workspace_services/user_resources/operation.py +++ b/cli/tre/commands/workspaces/workspace_services/user_resources/operation.py @@ -7,7 +7,7 @@ from .contexts import pass_user_resource_operation_context, UserResourceOperationContext -def operation_id_completion(ctx: click.Context, param, incomplete): +def operation_id_completion(ctx: click.Context, param: click.Parameter, incomplete: str): log = logging.getLogger(__name__) parent_ctx = ctx.parent user_resource_id = parent_ctx.params["user_resource_id"] diff --git a/cli/tre/commands/workspaces/workspace_services/user_resources/user_resource.py b/cli/tre/commands/workspaces/workspace_services/user_resources/user_resource.py index 5f804d701d..b7c2e6a33c 100644 --- a/cli/tre/commands/workspaces/workspace_services/user_resources/user_resource.py +++ b/cli/tre/commands/workspaces/workspace_services/user_resources/user_resource.py @@ -10,7 +10,7 @@ from .operations import user_resource_operations -def user_resource_id_completion(ctx: click.Context, param, incomplete): +def user_resource_id_completion(ctx: click.Context, param: click.Parameter, incomplete: str): log = logging.getLogger(__name__) parent_ctx = ctx.parent workspace_service_id = parent_ctx.params["workspace_service_id"] @@ -78,7 +78,7 @@ def user_resource_show( ) output( - response.text, + response, output_format=output_format, query=query, default_table_query=r"userResource.{id:id, template_name:templateName, template_version:templateVersion, display_name:properties.display_name, owner:user.name}", @@ -145,7 +145,7 @@ def user_resource_update( if no_wait: output( - response.text, + response, output_format=output_format, query=query, default_table_query=default_operation_table_query_single(), @@ -205,9 +205,9 @@ def user_resource_set_enabled( ) if no_wait: - if not suppress_output: + if not suppress_output or not response.is_success: output( - response.text, + response, output_format=output_format, query=query, default_table_query=default_operation_table_query_single(), @@ -295,7 +295,7 @@ def user_resource_delete( if no_wait: output( - response.text, + response, output_format=output_format, query=query, default_table_query=default_operation_table_query_single(), @@ -312,11 +312,60 @@ def user_resource_delete( ) +@click.command(name="invoke-action", help="Invoke an action on a user resource") +@click.argument("action-name", required=True) +@click.option("--no-wait", flag_value=True, default=False) +@output_option() +@query_option() +@pass_user_resource_context +def user_resource_invoke_action( + user_resource_context: UserResourceContext, + action_name, + no_wait, + output_format, + query, +): + log = logging.getLogger(__name__) + + workspace_id = user_resource_context.workspace_id + if workspace_id is None: + raise click.UsageError("Missing workspace ID") + workspace_service_id = user_resource_context.workspace_service_id + if workspace_service_id is None: + raise click.UsageError("Missing workspace service ID") + user_resource_id = user_resource_context.user_resource_id + if user_resource_id is None: + raise click.UsageError("Missing user resource ID") + + client = ApiClient.get_api_client_from_config() + workspace_scope = client.get_workspace_scope(log, workspace_id) + + click.echo(f"Invoking action {action_name}...\n", err=True) + response = client.call_api( + log, + "POST", + f"/api/workspaces/{workspace_id}/workspace-services/{workspace_service_id}/user-resources/{user_resource_id}/invoke-action", + scope_id=workspace_scope, + params={"action": action_name}, + ) + if no_wait: + output(response, output_format=output_format, query=query) + else: + operation_url = response.headers["location"] + operation_show( + log, + operation_url, + no_wait=False, + output_format=output_format, + query=query, + scope_id=workspace_scope, + ) + + user_resource.add_command(user_resource_show) user_resource.add_command(user_resource_update) user_resource.add_command(user_resource_set_enabled) user_resource.add_command(user_resource_delete) user_resource.add_command(user_resource_operation) user_resource.add_command(user_resource_operations) - -# TODO - invoke action +user_resource.add_command(user_resource_invoke_action) diff --git a/cli/tre/commands/workspaces/workspace_services/user_resources/user_resources.py b/cli/tre/commands/workspaces/workspace_services/user_resources/user_resources.py index f99e0270e2..976da0b18a 100644 --- a/cli/tre/commands/workspaces/workspace_services/user_resources/user_resources.py +++ b/cli/tre/commands/workspaces/workspace_services/user_resources/user_resources.py @@ -37,7 +37,7 @@ def user_resources_list(workspace_service_context: WorkspaceServiceContext, outp f'/api/workspaces/{workspace_id}/workspace-services/{workspace_service_id}/user-resources', scope_id=workspace_scope, ) - output(response.text, output_format=output_format, query=query, default_table_query=r"userResources[].{id:id, template_name:templateName, template_version:templateVersion, display_name:properties.display_name, owner:user.name}") + output(response, output_format=output_format, query=query, default_table_query=r"userResources[].{id:id, template_name:templateName, template_version:templateVersion, display_name:properties.display_name, owner:user.name}") @click.command(name="new", help="Create a new user resource") @@ -78,7 +78,7 @@ def user_resouce_create(workspace_service_context: WorkspaceServiceContext, defi ) if no_wait: - output(response.text, output_format=output_format, query=query) + output(response, output_format=output_format, query=query) return response.text else: operation_url = response.headers['location'] diff --git a/cli/tre/commands/workspaces/workspace_services/workspace_service.py b/cli/tre/commands/workspaces/workspace_services/workspace_service.py index ccfe72f1e0..cf1bce9783 100644 --- a/cli/tre/commands/workspaces/workspace_services/workspace_service.py +++ b/cli/tre/commands/workspaces/workspace_services/workspace_service.py @@ -12,7 +12,7 @@ from .user_resources.user_resources import user_resources -def workspace_service_id_completion(ctx: click.Context, param, incomplete): +def workspace_service_id_completion(ctx: click.Context, param: click.Parameter, incomplete: str): log = logging.getLogger(__name__) parent_ctx = ctx.parent workspace_id = parent_ctx.params["workspace_id"] @@ -55,7 +55,7 @@ def workspace_service_show(workspace_service_context: WorkspaceServiceContext, o scope_id=workspace_scope, ) - output(response.text, output_format=output_format, query=query, default_table_query=r"workspaceService.{id:id,template_name:templateName,template_version:templateVersion,sdeployment_status:deploymentStatus}") + output(response, output_format=output_format, query=query, default_table_query=r"workspaceService.{id:id,template_name:templateName,template_version:templateVersion,sdeployment_status:deploymentStatus}") @click.command(name="update", help="Update a workspace service") @@ -98,7 +98,7 @@ def workspace_service_update(workspace_service_context: WorkspaceServiceContext, scope_id=workspace_scope) if no_wait: - output(response.text, output_format=output_format, query=query, default_table_query=default_operation_table_query_single()) + output(response, output_format=output_format, query=query, default_table_query=default_operation_table_query_single()) else: operation_url = response.headers['location'] operation_show( @@ -144,8 +144,8 @@ def workspace_service_set_enabled(workspace_service_context: WorkspaceServiceCon scope_id=workspace_scope) if no_wait: - if not suppress_output: - output(response.text, output_format=output_format, query=query, default_table_query=default_operation_table_query_single()) + if not suppress_output or not response.is_success: + output(response, output_format=output_format, query=query, default_table_query=default_operation_table_query_single()) else: operation_url = response.headers['location'] operation_show( @@ -158,15 +158,119 @@ def workspace_service_set_enabled(workspace_service_context: WorkspaceServiceCon scope_id=workspace_scope) +@click.command(name="delete", help="Delete a workspace service") +@click.option('--yes', is_flag=True, default=False) +@click.option('--no-wait', + flag_value=True, + default=False) +@click.option('--ensure-disabled', + help="Disable before deleting if not currently enabled", + flag_value=True, + default=False) +@output_option() +@query_option() +@click.pass_context +@pass_workspace_service_context +def workspace_service_delete(workspace_service_context: WorkspaceServiceContext, ctx: click.Context, yes, no_wait, ensure_disabled, output_format, query): + log = logging.getLogger(__name__) + + workspace_id = workspace_service_context.workspace_id + if workspace_id is None: + raise click.UsageError('Missing workspace ID') + workspace_service_id = workspace_service_context.workspace_service_id + if workspace_service_id is None: + raise click.UsageError('Missing service ID') + + if not yes: + click.confirm("Are you sure you want to delete this workspace service?", err=True, abort=True) + + client = ApiClient.get_api_client_from_config() + workspace_scope = client.get_workspace_scope(log, workspace_id) + + if ensure_disabled: + response = client.call_api( + log, + 'GET', + f'/api/workspaces/{workspace_id}/workspace-services/{workspace_service_id}', + scope_id=workspace_scope) + workspace_service_json = response.json() + if workspace_service_json['workspaceService']['isEnabled']: + etag = workspace_service_json['workspaceService']['_etag'] + ctx.invoke( + workspace_service_set_enabled, + etag=etag, + enable=False, + no_wait=False, + suppress_output=True + ) + + click.echo("Deleting workspace service...", err=True) + response = client.call_api( + log, + 'DELETE', + f'/api/workspaces/{workspace_id}/workspace-services/{workspace_service_id}', + scope_id=workspace_scope) + + if no_wait: + output(response, output_format=output_format, query=query, default_table_query=default_operation_table_query_single()) + else: + operation_url = response.headers['location'] + operation_show(log, operation_url, no_wait, output_format=output_format, query=query, scope_id=workspace_scope) + + +@click.command(name="invoke-action", help="Invoke an action on a workspace service") +@click.argument("action-name", required=True) +@click.option("--no-wait", flag_value=True, default=False) +@output_option() +@query_option() +@pass_workspace_service_context +def workspace_service_invoke_action( + workspace_service_context: WorkspaceServiceContext, + action_name, + no_wait, + output_format, + query, +): + log = logging.getLogger(__name__) + + workspace_id = workspace_service_context.workspace_id + if workspace_id is None: + raise click.UsageError('Missing workspace ID') + workspace_service_id = workspace_service_context.workspace_service_id + if workspace_service_id is None: + raise click.UsageError('Missing service ID') + + client = ApiClient.get_api_client_from_config() + workspace_scope = client.get_workspace_scope(log, workspace_id) + + click.echo(f"Invoking action {action_name}...\n", err=True) + response = client.call_api( + log, + "POST", + f"/api/workspaces/{workspace_id}/workspace-services/{workspace_service_id}/invoke-action", + scope_id=workspace_scope, + params={"action": action_name}, + ) + if no_wait: + output(response, output_format=output_format, query=query) + else: + operation_url = response.headers["location"] + operation_show( + log, + operation_url, + no_wait=False, + output_format=output_format, + query=query, + scope_id=workspace_scope, + ) + + workspace_service.add_command(workspace_service_show) workspace_service.add_command(workspace_service_update) workspace_service.add_command(workspace_service_set_enabled) workspace_service.add_command(workspace_service_operation) workspace_service.add_command(workspace_service_operations) +workspace_service.add_command(workspace_service_delete) +workspace_service.add_command(workspace_service_invoke_action) workspace_service.add_command(user_resource) workspace_service.add_command(user_resources) - - -# TODO delete - -# TODO - invoke action diff --git a/cli/tre/commands/workspaces/workspace_services/workspace_services.py b/cli/tre/commands/workspaces/workspace_services/workspace_services.py index 032a2dcbe8..e32e14cb34 100644 --- a/cli/tre/commands/workspaces/workspace_services/workspace_services.py +++ b/cli/tre/commands/workspaces/workspace_services/workspace_services.py @@ -34,7 +34,7 @@ def workspace_services_list(workspace_context, output_format, query): f'/api/workspaces/{workspace_id}/workspace-services', scope_id=workspace_scope, ) - output(response.text, output_format=output_format, query=query, default_table_query=r"workspaceServices[].{id:id,template_name:templateName,template_version:templateVersion,sdeployment_status:deploymentStatus}") + output(response, output_format=output_format, query=query, default_table_query=r"workspaceServices[].{id:id,template_name:templateName,template_version:templateVersion,sdeployment_status:deploymentStatus}") @click.command(name="new", help="Create a new workspace-service") @@ -72,7 +72,7 @@ def workspace_services_create(workspace_context: WorkspaceContext, definition, d ) if no_wait: - output(response.text, output_format=output_format, query=query) + output(response, output_format=output_format, query=query) return response.text else: operation_url = response.headers['location'] diff --git a/cli/tre/commands/workspaces/workspaces.py b/cli/tre/commands/workspaces/workspaces.py index da0459e2dc..5ad7c1a322 100644 --- a/cli/tre/commands/workspaces/workspaces.py +++ b/cli/tre/commands/workspaces/workspaces.py @@ -21,7 +21,7 @@ def workspaces_list(output_format, query): client = ApiClient.get_api_client_from_config() response = client.call_api(log, 'GET', '/api/workspaces') output( - response.text, + response, output_format=output_format, query=query, default_table_query=r"workspaces[].{id:id, display_name:properties.display_name, deployment_status:deploymentStatus, workspace_url:workspaceURL}") @@ -52,7 +52,7 @@ def workspaces_create(ctx, definition, definition_file, no_wait, output_format, response = client.call_api(log, 'POST', '/api/workspaces', json_data=definition_dict) if no_wait: - output(response.text, output_format=output_format, query=query, default_table_query=default_operation_table_query_single()) + output(response, output_format=output_format, query=query, default_table_query=default_operation_table_query_single()) return response.text else: operation_url = response.headers['location'] diff --git a/cli/tre/main.py b/cli/tre/main.py index 78ce55628e..26aa27886b 100644 --- a/cli/tre/main.py +++ b/cli/tre/main.py @@ -1,6 +1,7 @@ import click from .commands.costs import costs from .commands.health import health +from .commands.migrations import migrations from tre.commands.get_token import get_token from tre.commands.login import login @@ -43,7 +44,7 @@ def cli(): cli.add_command(health) -# TODO - migrations? +cli.add_command(migrations) if __name__ == "__main__": cli() diff --git a/cli/tre/output.py b/cli/tre/output.py index a66abab5b6..9714d0d2fc 100644 --- a/cli/tre/output.py +++ b/cli/tre/output.py @@ -1,3 +1,4 @@ +import sys import click import jmespath import json @@ -5,6 +6,7 @@ from tabulate import tabulate from pygments import highlight, lexers, formatters from enum import Enum +from httpx import Response class OutputFormat(Enum): @@ -12,12 +14,13 @@ class OutputFormat(Enum): Json = 'json' JsonC = 'jsonc' Table = 'table' + Raw = 'raw' def output_option(*param_decls: str, **kwargs: t.Any): param_decls = ('--output', '-o', 'output_format') kwargs.setdefault("default", 'table') - kwargs.setdefault("type", click.Choice(['table', 'json', 'jsonc', 'none'])) + kwargs.setdefault("type", click.Choice(['table', 'json', 'jsonc', 'raw', 'none'])) kwargs.setdefault("envvar", "TRECLI_OUTPUT") kwargs.setdefault("help", "Output format") return click.option(*param_decls, **kwargs) @@ -30,10 +33,15 @@ def query_option(*param_decls: str, **kwargs: t.Any): return click.option(*param_decls, **kwargs) -def output(result_json, output_format: OutputFormat = OutputFormat.Json, query: str = None, default_table_query: str = None) -> None: +def output(response: Response, output_format: OutputFormat = OutputFormat.Json, query: str = None, default_table_query: str = None) -> None: + if output_format == OutputFormat.Suppress.value: + if not response.is_success: + sys.exit(1) return + result_json = response.text + if query is None and output_format == OutputFormat.Table.value: query = default_table_query @@ -50,6 +58,9 @@ def output(result_json, output_format: OutputFormat = OutputFormat.Json, query: formatted_json = json.dumps(json.loads(output_json), sort_keys=False, indent=2) jsonc = highlight(formatted_json, lexers.JsonLexer(), formatters.TerminalFormatter()) click.echo(jsonc) + elif output_format == OutputFormat.Raw.value: + value = json.loads(output_json) + click.echo(value) elif output_format == OutputFormat.Table.value: content = json.loads(output_json) if content is not None: @@ -80,3 +91,6 @@ def output(result_json, output_format: OutputFormat = OutputFormat.Json, query: click.echo(tabulate(rows, columns)) else: raise click.ClickException(f"Unhandled output format: '{output_format}'") + + if not response.is_success: + sys.exit(1)