From 2b72b857c4be169e07fe85f6fc0e06dad0e6e613 Mon Sep 17 00:00:00 2001 From: Derek Schlabach Date: Fri, 15 Dec 2023 12:13:25 -0600 Subject: [PATCH 1/9] globus gcs endpoint set-subscription-id --- src/globus_cli/commands/gcp/__init__.py | 4 ++ src/globus_cli/commands/gcs/__init__.py | 5 +- .../commands/gcs/endpoint/__init__.py | 11 +++ .../gcs/endpoint/set_subscription_id.py | 72 +++++++++++++++++++ tests/functional/gcs/__init__.py | 0 tests/functional/gcs/endpoint/__init__.py | 0 .../gcs/endpoint/test_set_subscription_id.py | 64 +++++++++++++++++ 7 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 src/globus_cli/commands/gcs/endpoint/__init__.py create mode 100644 src/globus_cli/commands/gcs/endpoint/set_subscription_id.py create mode 100644 tests/functional/gcs/__init__.py create mode 100644 tests/functional/gcs/endpoint/__init__.py create mode 100644 tests/functional/gcs/endpoint/test_set_subscription_id.py diff --git a/src/globus_cli/commands/gcp/__init__.py b/src/globus_cli/commands/gcp/__init__.py index ef094b7e4..33cfaa482 100644 --- a/src/globus_cli/commands/gcp/__init__.py +++ b/src/globus_cli/commands/gcp/__init__.py @@ -6,6 +6,10 @@ lazy_subcommands={ "create": (".create", "create_command"), "update": (".update", "update_command"), + "set-subscription-id": ( + "endpoint.set_subscription_id", + "set_endpoint_subscription_id", + ), }, ) def gcp_command() -> None: diff --git a/src/globus_cli/commands/gcs/__init__.py b/src/globus_cli/commands/gcs/__init__.py index 7a0457136..5b3a23c8a 100644 --- a/src/globus_cli/commands/gcs/__init__.py +++ b/src/globus_cli/commands/gcs/__init__.py @@ -5,9 +5,12 @@ "gcs", lazy_subcommands={ "collection": ("collection", "collection_command"), + # Note: endpoint is not an alias for the root 'endpoint' group as that group is + # broken up into slightly different subcommand structures here. + "endpoint": (".endpoint", "endpoint_command"), "storage-gateway": ("endpoint.storage_gateway", "storage_gateway_command"), "user-credential": ("endpoint.user_credential", "user_credential_command"), }, ) def gcs_command() -> None: - """Manage Globus Connect Server endpoints""" + """Manage Globus Connect Server (GCS) resources""" diff --git a/src/globus_cli/commands/gcs/endpoint/__init__.py b/src/globus_cli/commands/gcs/endpoint/__init__.py new file mode 100644 index 000000000..a85a584d6 --- /dev/null +++ b/src/globus_cli/commands/gcs/endpoint/__init__.py @@ -0,0 +1,11 @@ +from globus_cli.parsing import group + + +@group( + "endpoint", + lazy_subcommands={ + "set-subscription-id": (".set_subscription_id", "set_subscription_id_command"), + }, +) +def endpoint_command() -> None: + """Manage Globus Connect Server (GCS) endpoints""" diff --git a/src/globus_cli/commands/gcs/endpoint/set_subscription_id.py b/src/globus_cli/commands/gcs/endpoint/set_subscription_id.py new file mode 100644 index 000000000..e63e350fe --- /dev/null +++ b/src/globus_cli/commands/gcs/endpoint/set_subscription_id.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import typing as t +import uuid + +import click + +from globus_cli.constants import EXPLICIT_NULL, ExplicitNullType +from globus_cli.login_manager import LoginManager +from globus_cli.parsing import command, endpoint_id_arg +from globus_cli.termio import TextMode, display + + +class GCSSubscriptionIdType(click.ParamType): + def get_type_annotation(self, _: click.Parameter) -> type: + return t.cast(type, uuid.UUID | t.Literal["DEFAULT"] | ExplicitNullType) + + def convert( + self, value: str, param: click.Parameter | None, ctx: click.Context | None + ) -> t.Any: + if value is None or (ctx and ctx.resilient_parsing): + return None + if value.lower() == "null": + return EXPLICIT_NULL + elif value.lower() == "default": + return "DEFAULT" + try: + uuid.UUID(value) + return value + except ValueError: + msg = ( + f"{value} is invalid. Expected either a UUID or the special " + 'values "DEFAULT" or "null"' + ) + self.fail(msg, param, ctx) + + +@command("set-subscription-id", short_help="Update an endpoint's subscription") +@endpoint_id_arg +@click.argument("SUBSCRIPTION_ID", type=GCSSubscriptionIdType()) +@LoginManager.requires_login("transfer") +def set_subscription_id_command( + login_manager: LoginManager, + *, + endpoint_id: uuid.UUID, + subscription_id: uuid.UUID | t.Literal["DEFAULT"] | ExplicitNullType, +) -> None: + """ + Update an endpoint's subscription ID. + + SUBSCRIPTION_ID must be one of: (1) A valid subscription ID (UUID), (2) the value + "DEFAULT" (requires that you manage exactly one subscription & assigns the endpoint + to that subscription), or (3) the value "null" (clears the endpoint's subscription) + + Setting a subscription requires that you are a subscription manager for the + subscription group being assigned. + + Removing a subscription requires that you are either (1) a subscription manager for + the current assigned subscription group or (2) an admin of the endpoint. + """ + gcs_client = login_manager.get_gcs_client(endpoint_id=endpoint_id) + + subscription_id_val = None if subscription_id is EXPLICIT_NULL else subscription_id + res = gcs_client.put( + "/endpoint/subscription_id", + data={ + "DATA_TYPE": "endpoint_subscription#1.0.0", + "subscription_id": subscription_id_val, + }, + ) + + display(res, text_mode=TextMode.text_raw, response_key="message") diff --git a/tests/functional/gcs/__init__.py b/tests/functional/gcs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/functional/gcs/endpoint/__init__.py b/tests/functional/gcs/endpoint/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/functional/gcs/endpoint/test_set_subscription_id.py b/tests/functional/gcs/endpoint/test_set_subscription_id.py new file mode 100644 index 000000000..96d501c4a --- /dev/null +++ b/tests/functional/gcs/endpoint/test_set_subscription_id.py @@ -0,0 +1,64 @@ +import uuid + +import pytest +import responses +from globus_sdk._testing import load_response_set + + +@pytest.mark.parametrize("subscription_id", (str(uuid.uuid4()), "DEFAULT", "null")) +def test_gcs_endpoint_set_subscription_id(subscription_id, run_line, add_gcs_login): + meta = load_response_set("cli.collection_operations").metadata + endpoint_id = meta["endpoint_id"] + gcs_hostname = meta["gcs_hostname"] + add_gcs_login(endpoint_id) + + responses.put( + f"https://{gcs_hostname}/api/endpoint/subscription_id", + json={ + "DATA_TYPE": "result#1.0.0", + "code": "success", + "detail": "success", + "has_next_page": False, + "http_response_code": 200, + "message": f"Updated Endpoint {endpoint_id}", + }, + ) + + result = run_line( + f"globus gcs endpoint set-subscription-id {endpoint_id} {subscription_id}" + ) + + assert f"Updated Endpoint {endpoint_id}" in result.stdout + + +def test_gcs_endpoint_set_subscription_id__when_not_subscription_manager( + run_line, add_gcs_login +): + meta = load_response_set("cli.collection_operations").metadata + endpoint_id = meta["endpoint_id"] + gcs_hostname = meta["gcs_hostname"] + add_gcs_login(endpoint_id) + + error_message = ( + "Unable to use DEFAULT subscription. Your identity does not manage any" + "subscriptions" + ) + responses.put( + f"https://{gcs_hostname}/api/endpoint/subscription_id", + status=400, + json={ + "DATA_TYPE": "result#1.0.0", + "code": "bad_request", + "detail": "bad_request", + "has_next_page": False, + "http_response_code": 400, + "message": error_message, + }, + ) + + result = run_line( + f"globus gcs endpoint set-subscription-id {endpoint_id} DEFAULT", + assert_exit_code=1, + ) + + assert error_message in result.stderr From d80b0dfecbb8f5b13c670e2ea7406652b8e4a5b5 Mon Sep 17 00:00:00 2001 From: Derek Schlabach Date: Fri, 15 Dec 2023 14:05:31 -0600 Subject: [PATCH 2/9] Scriv --- ...31215_140223_derek_gcs_subscription_id_sc_28587.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 changelog.d/20231215_140223_derek_gcs_subscription_id_sc_28587.md diff --git a/changelog.d/20231215_140223_derek_gcs_subscription_id_sc_28587.md b/changelog.d/20231215_140223_derek_gcs_subscription_id_sc_28587.md new file mode 100644 index 000000000..dc54a4947 --- /dev/null +++ b/changelog.d/20231215_140223_derek_gcs_subscription_id_sc_28587.md @@ -0,0 +1,11 @@ + +### Enhancements + +* `[sc-28587] `_ + Added a new command `globus gcs endpoint set-subscription-id` which allows + subscription managers and endpoint admins to modify the subscription ID for a + gcs endpoint. + +* `[sc-28587] `_ + Added a new command `globus gcp set-subscription-id` which allows subscription + managers and collection admins to modify the subscription ID for a gcp collection. From e47278cde3c55e686ce3b985c23af6ae46c16376 Mon Sep 17 00:00:00 2001 From: derek-globus <113056046+derek-globus@users.noreply.github.com> Date: Wed, 20 Dec 2023 13:50:11 -0600 Subject: [PATCH 3/9] Update changelog.d/20231215_140223_derek_gcs_subscription_id_sc_28587.md Co-authored-by: Kurt McKee --- .../20231215_140223_derek_gcs_subscription_id_sc_28587.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20231215_140223_derek_gcs_subscription_id_sc_28587.md b/changelog.d/20231215_140223_derek_gcs_subscription_id_sc_28587.md index dc54a4947..1576211ec 100644 --- a/changelog.d/20231215_140223_derek_gcs_subscription_id_sc_28587.md +++ b/changelog.d/20231215_140223_derek_gcs_subscription_id_sc_28587.md @@ -4,7 +4,7 @@ * `[sc-28587] `_ Added a new command `globus gcs endpoint set-subscription-id` which allows subscription managers and endpoint admins to modify the subscription ID for a - gcs endpoint. + GCS endpoint. * `[sc-28587] `_ Added a new command `globus gcp set-subscription-id` which allows subscription From 31e92faac5b663e8ea841d92ed1c69a482e888f0 Mon Sep 17 00:00:00 2001 From: derek-globus <113056046+derek-globus@users.noreply.github.com> Date: Wed, 20 Dec 2023 13:50:17 -0600 Subject: [PATCH 4/9] Update changelog.d/20231215_140223_derek_gcs_subscription_id_sc_28587.md Co-authored-by: Kurt McKee --- .../20231215_140223_derek_gcs_subscription_id_sc_28587.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/20231215_140223_derek_gcs_subscription_id_sc_28587.md b/changelog.d/20231215_140223_derek_gcs_subscription_id_sc_28587.md index 1576211ec..41d8622e1 100644 --- a/changelog.d/20231215_140223_derek_gcs_subscription_id_sc_28587.md +++ b/changelog.d/20231215_140223_derek_gcs_subscription_id_sc_28587.md @@ -8,4 +8,4 @@ * `[sc-28587] `_ Added a new command `globus gcp set-subscription-id` which allows subscription - managers and collection admins to modify the subscription ID for a gcp collection. + managers and collection admins to modify the subscription ID for a GCP collection. From e81ba1f0638a1331ed3f7815ef56aaf3ba63d634 Mon Sep 17 00:00:00 2001 From: Derek Schlabach Date: Wed, 20 Dec 2023 14:00:41 -0600 Subject: [PATCH 5/9] PR comments: --- src/globus_cli/commands/gcs/endpoint/set_subscription_id.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/globus_cli/commands/gcs/endpoint/set_subscription_id.py b/src/globus_cli/commands/gcs/endpoint/set_subscription_id.py index e63e350fe..c50c38268 100644 --- a/src/globus_cli/commands/gcs/endpoint/set_subscription_id.py +++ b/src/globus_cli/commands/gcs/endpoint/set_subscription_id.py @@ -18,8 +18,9 @@ def get_type_annotation(self, _: click.Parameter) -> type: def convert( self, value: str, param: click.Parameter | None, ctx: click.Context | None ) -> t.Any: - if value is None or (ctx and ctx.resilient_parsing): + if ctx and ctx.resilient_parsing: return None + if value.lower() == "null": return EXPLICIT_NULL elif value.lower() == "default": @@ -46,7 +47,7 @@ def set_subscription_id_command( subscription_id: uuid.UUID | t.Literal["DEFAULT"] | ExplicitNullType, ) -> None: """ - Update an endpoint's subscription ID. + Update an endpoint's subscription. SUBSCRIPTION_ID must be one of: (1) A valid subscription ID (UUID), (2) the value "DEFAULT" (requires that you manage exactly one subscription & assigns the endpoint From c8bd24d85aac29426c12cdfa6c15fe0e45b69506 Mon Sep 17 00:00:00 2001 From: Derek Schlabach Date: Wed, 20 Dec 2023 14:24:33 -0600 Subject: [PATCH 6/9] Wording Update --- src/globus_cli/commands/gcs/endpoint/set_subscription_id.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/globus_cli/commands/gcs/endpoint/set_subscription_id.py b/src/globus_cli/commands/gcs/endpoint/set_subscription_id.py index c50c38268..79d93c861 100644 --- a/src/globus_cli/commands/gcs/endpoint/set_subscription_id.py +++ b/src/globus_cli/commands/gcs/endpoint/set_subscription_id.py @@ -54,7 +54,7 @@ def set_subscription_id_command( to that subscription), or (3) the value "null" (clears the endpoint's subscription) Setting a subscription requires that you are a subscription manager for the - subscription group being assigned. + subscription being assigned. Removing a subscription requires that you are either (1) a subscription manager for the current assigned subscription group or (2) an admin of the endpoint. From a0454e4a3e0833df76f9a2d9824e5a0a3305e6c4 Mon Sep 17 00:00:00 2001 From: Derek Schlabach Date: Tue, 2 Jan 2024 16:43:45 -0600 Subject: [PATCH 7/9] Dedicated globus gcp set-subscription-id implementation --- .../commands/endpoint/set_subscription_id.py | 11 ++- src/globus_cli/commands/gcp/__init__.py | 5 +- .../commands/gcp/set_subscription_id.py | 68 +++++++++++++++++++ .../gcs/endpoint/set_subscription_id.py | 5 +- 4 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 src/globus_cli/commands/gcp/set_subscription_id.py diff --git a/src/globus_cli/commands/endpoint/set_subscription_id.py b/src/globus_cli/commands/endpoint/set_subscription_id.py index 62ca3d540..dc603208e 100644 --- a/src/globus_cli/commands/endpoint/set_subscription_id.py +++ b/src/globus_cli/commands/endpoint/set_subscription_id.py @@ -28,7 +28,11 @@ def convert( self.fail(f"{value} is not a valid Subscription ID", param, ctx) -@command("set-subscription-id", short_help="Set an endpoint's subscription") +@command( + "set-subscription-id", + deprecated=True, + short_help="Set an endpoint's subscription", +) @endpoint_id_arg @click.argument("SUBSCRIPTION_ID", type=SubscriptionIdType()) @LoginManager.requires_login("transfer") @@ -36,6 +40,11 @@ def set_endpoint_subscription_id( login_manager: LoginManager, *, endpoint_id: uuid.UUID, subscription_id: str ) -> None: """ + For GCS endpoints, refer to ``globus gcs endpoint set-subscription-id``. For + GCP endpoint, refer ``globus gcp set-subscription-id``. + + ----------------------------- + Set an endpoint's subscription ID. Unlike the '--managed' flag for 'globus endpoint update', this operation does not diff --git a/src/globus_cli/commands/gcp/__init__.py b/src/globus_cli/commands/gcp/__init__.py index 33cfaa482..5246eac36 100644 --- a/src/globus_cli/commands/gcp/__init__.py +++ b/src/globus_cli/commands/gcp/__init__.py @@ -6,10 +6,7 @@ lazy_subcommands={ "create": (".create", "create_command"), "update": (".update", "update_command"), - "set-subscription-id": ( - "endpoint.set_subscription_id", - "set_endpoint_subscription_id", - ), + "set-subscription-id": (".set_subscription_id", "set_endpoint_subscription_id"), }, ) def gcp_command() -> None: diff --git a/src/globus_cli/commands/gcp/set_subscription_id.py b/src/globus_cli/commands/gcp/set_subscription_id.py new file mode 100644 index 000000000..1e759333f --- /dev/null +++ b/src/globus_cli/commands/gcp/set_subscription_id.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +import typing as t +import uuid + +import click + +from globus_cli.constants import EXPLICIT_NULL, ExplicitNullType +from globus_cli.endpointish import Endpointish, EntityType +from globus_cli.login_manager import LoginManager +from globus_cli.parsing import command, endpoint_id_arg +from globus_cli.termio import TextMode, display + + +class GCPSubscriptionIdType(click.ParamType): + def get_type_annotation(self, _: click.Parameter) -> type: + return t.cast(type, uuid.UUID | ExplicitNullType) + + def convert( + self, value: str, param: click.Parameter | None, ctx: click.Context | None + ) -> t.Any: + if ctx and ctx.resilient_parsing: + return None + + if value.lower() == "null": + return EXPLICIT_NULL + + try: + return uuid.UUID(value) + except ValueError: + msg = ( + f"{value} is invalid. Expected either a UUID or the special value " + '"null"' + ) + self.fail(msg, param, ctx) + + +@command("set-subscription-id", short_help="Update a GCP endpoint's subscription") +@endpoint_id_arg +@click.argument("SUBSCRIPTION_ID", type=GCPSubscriptionIdType()) +@LoginManager.requires_login("transfer") +def set_endpoint_subscription_id( + login_manager: LoginManager, + *, + endpoint_id: uuid.UUID, + subscription_id: uuid.UUID | ExplicitNullType, +) -> None: + """ + Update a GCP endpoint's subscription. + + This operation does not require you to be an admin of the endpoint. It is useful in + cases where you are a subscription manager applying a subscription to an endpoint + administered by someone else. + + SUBSCRIPTION_ID must be one of: (1) A valid subscription ID (UUID) or (2) the value + "null" (clears the endpoint's subscription). + """ + transfer_client = login_manager.get_transfer_client() + epish = Endpointish(endpoint_id, transfer_client=transfer_client) + epish.assert_entity_type(expect_types=EntityType.GCP_MAPPED) + + sub_id = None if subscription_id is EXPLICIT_NULL else str(subscription_id) + res = transfer_client.put( + f"/endpoint/{endpoint_id}/subscription", + data={"subscription_id": sub_id}, + ) + + display(res, text_mode=TextMode.text_raw, response_key="message") diff --git a/src/globus_cli/commands/gcs/endpoint/set_subscription_id.py b/src/globus_cli/commands/gcs/endpoint/set_subscription_id.py index 79d93c861..593994206 100644 --- a/src/globus_cli/commands/gcs/endpoint/set_subscription_id.py +++ b/src/globus_cli/commands/gcs/endpoint/set_subscription_id.py @@ -26,8 +26,7 @@ def convert( elif value.lower() == "default": return "DEFAULT" try: - uuid.UUID(value) - return value + return uuid.UUID(value) except ValueError: msg = ( f"{value} is invalid. Expected either a UUID or the special " @@ -51,7 +50,7 @@ def set_subscription_id_command( SUBSCRIPTION_ID must be one of: (1) A valid subscription ID (UUID), (2) the value "DEFAULT" (requires that you manage exactly one subscription & assigns the endpoint - to that subscription), or (3) the value "null" (clears the endpoint's subscription) + to that subscription), or (3) the value "null" (clears the endpoint's subscription). Setting a subscription requires that you are a subscription manager for the subscription being assigned. From 0c9a247b435a5e7616cf1c95933bc23617989b45 Mon Sep 17 00:00:00 2001 From: derek-globus <113056046+derek-globus@users.noreply.github.com> Date: Thu, 4 Jan 2024 10:41:24 -0600 Subject: [PATCH 8/9] Update src/globus_cli/commands/gcp/set_subscription_id.py Co-authored-by: Stephen Rosen --- src/globus_cli/commands/gcp/set_subscription_id.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/globus_cli/commands/gcp/set_subscription_id.py b/src/globus_cli/commands/gcp/set_subscription_id.py index 1e759333f..b4e897acf 100644 --- a/src/globus_cli/commands/gcp/set_subscription_id.py +++ b/src/globus_cli/commands/gcp/set_subscription_id.py @@ -59,10 +59,9 @@ def set_endpoint_subscription_id( epish = Endpointish(endpoint_id, transfer_client=transfer_client) epish.assert_entity_type(expect_types=EntityType.GCP_MAPPED) - sub_id = None if subscription_id is EXPLICIT_NULL else str(subscription_id) res = transfer_client.put( f"/endpoint/{endpoint_id}/subscription", - data={"subscription_id": sub_id}, + data={"subscription_id": ExplicitNullType.nullify(subscription_id)}, ) display(res, text_mode=TextMode.text_raw, response_key="message") From dc66ed8a59a1ed8d5ee7a1e5335c5aa6ef5776e4 Mon Sep 17 00:00:00 2001 From: Derek Schlabach Date: Thu, 4 Jan 2024 11:25:57 -0600 Subject: [PATCH 9/9] Minor wording update --- src/globus_cli/commands/endpoint/set_subscription_id.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/globus_cli/commands/endpoint/set_subscription_id.py b/src/globus_cli/commands/endpoint/set_subscription_id.py index dc603208e..278bf25db 100644 --- a/src/globus_cli/commands/endpoint/set_subscription_id.py +++ b/src/globus_cli/commands/endpoint/set_subscription_id.py @@ -41,7 +41,7 @@ def set_endpoint_subscription_id( ) -> None: """ For GCS endpoints, refer to ``globus gcs endpoint set-subscription-id``. For - GCP endpoint, refer ``globus gcp set-subscription-id``. + GCP endpoints, refer to ``globus gcp set-subscription-id``. -----------------------------