diff --git a/changelog.d/20251205_152802_aaschaer_domain.md b/changelog.d/20251205_152802_aaschaer_domain.md new file mode 100644 index 00000000..0f8a4ddb --- /dev/null +++ b/changelog.d/20251205_152802_aaschaer_domain.md @@ -0,0 +1,3 @@ +### Enhancements + +* Add Domain field to default output of `globus endpoint search` and `globus endpoint show` diff --git a/src/globus_cli/commands/endpoint/show.py b/src/globus_cli/commands/endpoint/show.py index 8f90f234..386f29c9 100644 --- a/src/globus_cli/commands/endpoint/show.py +++ b/src/globus_cli/commands/endpoint/show.py @@ -5,12 +5,14 @@ from globus_cli.endpointish import Endpointish from globus_cli.login_manager import LoginManager from globus_cli.parsing import command, endpoint_id_arg +from globus_cli.services.transfer import DOMAIN_FIELD from globus_cli.termio import Field, display, formatters STANDARD_FIELDS = [ Field("Display Name", "display_name"), Field("ID", "id"), Field("Owner", "owner_string"), + DOMAIN_FIELD, Field("Description", "description", wrap_enabled=True), Field("Shareable", "shareable"), Field("Department", "department"), diff --git a/src/globus_cli/services/transfer/__init__.py b/src/globus_cli/services/transfer/__init__.py index d10e58d3..26ade9bd 100644 --- a/src/globus_cli/services/transfer/__init__.py +++ b/src/globus_cli/services/transfer/__init__.py @@ -19,15 +19,46 @@ def parse(self, value: t.Any) -> str: return str(value[0] or value[1]) +class _DomainFormatter(formatters.StrFormatter): + def parse(self, value: t.Any) -> str: + if not isinstance(value, list) or len(value) != 2: + raise ValueError("cannot parse domain from malformed data") + + tlsftp_server, gcs_manager_url = value + + # if tlsftp_server is present, this is a GCS collection + # parse tlsftp_server in the format tlsftp://{domain}:{port} + if tlsftp_server: + assert isinstance(tlsftp_server, str) + return tlsftp_server[len("tlsftp://") :].split(":")[0] + + # if gcs_manager_url present, but not tlsftp_server, this is a GCS endpoint + # parse gcs_manager_url in the format https://{domain} + elif gcs_manager_url: + assert isinstance(gcs_manager_url, str) + return gcs_manager_url[len("https://") :] + + # entity type with no domain + else: + return str(None) + + +DOMAIN_FIELD = Field( + "Domain", "[tlsftp_server, gcs_manager_url]", formatter=_DomainFormatter() +) + + ENDPOINT_LIST_FIELDS = [ Field("ID", "id"), Field("Owner", "owner_string"), Field("Display Name", "[display_name, canonical_name]", formatter=_NameFormatter()), + DOMAIN_FIELD, ] __all__ = ( "ENDPOINT_LIST_FIELDS", + "DOMAIN_FIELD", "CustomTransferClient", "RecursiveLsResponse", "display_name_or_cname", diff --git a/tests/functional/endpoint/test_endpoint_search.py b/tests/functional/endpoint/test_endpoint_search.py index 96f8b7da..9094b31f 100644 --- a/tests/functional/endpoint/test_endpoint_search.py +++ b/tests/functional/endpoint/test_endpoint_search.py @@ -15,13 +15,13 @@ def _make_mapped_collection_search_result( endpoint_id: str, display_name: str, owner_string: str, + manager_fqdn: str, + collection_fqdn: str, ) -> dict[str, t.Any]: # most of the fields are filled with dummy data # some of these values are pulled out here either to ensure their integrity # or to make them more visible to a reader username = "u_abcdefghijklmnop" # not a real b32 username - manager_fqdn = "a0bc1.23de.data.globus.org" - collection_fqdn = f"m-f45678.{manager_fqdn}" data = { "DATA_TYPE": "endpoint", @@ -123,6 +123,9 @@ def singular_search_response(): endpoint_id = str(uuid.uuid4()) display_name = "dummy result" owner_string = "globus@globus.org" + manager_fqdn = "a0bc1.23de.data.globus.org" + collection_fqdn = f"m-f45678.{manager_fqdn}" + return RegisteredResponse( service="transfer", path="/v0.10/endpoint_search", @@ -131,11 +134,17 @@ def singular_search_response(): "endpoint_id": endpoint_id, "display_name": display_name, "owner_string": owner_string, + "collection_fqdn": collection_fqdn, }, json={ "DATA": [ _make_mapped_collection_search_result( - collection_id, endpoint_id, display_name, owner_string + collection_id, + endpoint_id, + display_name, + owner_string, + manager_fqdn, + collection_fqdn, ) ], "DATA_TYPE": "endpoint_list", @@ -162,20 +171,21 @@ def test_search_shows_collection_id(run_line, singular_search_response): header_line, separator_line, data_line = lines # the header line shows the field names in order - header_row = re.split(r"\s+\|\s+", header_line) - assert header_row == ["ID", "Owner", "Display Name"] + header_row = [header.strip() for header in re.split(r"\s+\|\s+", header_line)] + assert header_row == ["ID", "Owner", "Display Name", "Domain"] # the separator line is a series of dashes separator_row = re.split(r"\s+\|\s+", separator_line) - assert len(separator_row) == 3 + assert len(separator_row) == 4 for separator in separator_row: assert set(separator) == {"-"} # exactly one character is used - # the data row should have the collection ID, Owner, and Display Name + # the data row should have the collection ID, Owner, Display Name, and Domain data_row = re.split(r"\s+\|\s+", data_line) assert data_row == [ meta["collection_id"], meta["owner_string"], meta["display_name"], + meta["collection_fqdn"], ] # final sanity check -- the endpoint ID for a mapped collection doesn't