Repository: PrefectHQ/prefect

Issue:
Add `prefect cloud logout` command
### First check

- [X] I added a descriptive title to this issue.
- [X] I used the GitHub search to find a similar request and didn't find it.
- [X] I searched the Prefect documentation for this feature.

### Prefect Version

2.x

### Describe the current behavior

Add `prefect cloud logout` to log out of a workspace 

### Describe the proposed behavior

Likely by unsetting the `PREFECT_API_KEY` and `PREFECT_API_URL` back to default

### Example Use

_No response_

### Additional context

_No response_


Discussion:
Is it necessary to keep any configuration in the profile after logout?

I ask this because the `prefect cloud login` command uses the `PREFECT_API_KEY` as a key to find an existing profile and update it:

https://github.com/PrefectHQ/prefect/blob/465f8e65cecbc20de04737d886b95166a37cd1b9/src/prefect/cli/cloud.py#L175-L178

And it also checks if the profile exists before saving the key to prevent an existing profile from being overwritten:

https://github.com/PrefectHQ/prefect/blob/465f8e65cecbc20de04737d886b95166a37cd1b9/src/prefect/cli/cloud.py#L217-L218

Therefore, setting `PREFECT_API_KEY` to the default value invalidates the entire profile.

If you don't need to maintain some configuration, the `prefect cloud logout` command could be an alias for `prefect profile delete`.

What do you think?
I'm not sure I love the behavior in the `prefect cloud login` command in the first place. I think the simplest way to implement this is:

- Check if the current profile has an API key set
- If not, error. Maybe display all the profiles that _are_ logged in.
- Otherwise, remove the API key and and API URL from the current profile

Resolving patch:
diff --git a/src/prefect/cli/cloud.py b/src/prefect/cli/cloud.py
--- a/src/prefect/cli/cloud.py
+++ b/src/prefect/cli/cloud.py
@@ -150,8 +150,6 @@ async def login(
     Creates a new profile configured to use the specified PREFECT_API_KEY.
     Uses a previously configured profile if it exists.
     """
-    profiles = load_profiles()
-
     async with get_cloud_client(api_key=key) as client:
         try:
             workspaces = await client.read_workspaces()
@@ -172,6 +170,8 @@ async def login(
         except httpx.HTTPStatusError as exc:
             exit_with_error(f"Error connecting to Prefect Cloud: {exc!r}")
 
+    profiles = load_profiles()
+
     for profile_name in profiles:
         if key == profiles[profile_name].settings.get(PREFECT_API_KEY):
             profiles.set_active(profile_name)
@@ -192,41 +192,19 @@ async def login(
                     )
                     current_workspace = workspace_handle
 
-                exit_with_success(
-                    f"Logged in to Prefect Cloud using profile {profile_name!r}.\n"
-                    f"Workspace is currently set to {current_workspace!r}. "
-                    f"The workspace can be changed using `prefect cloud workspace set`."
-                )
-
-    workspace_handle_details = {
-        f"{workspace['account_handle']}/{workspace['workspace_handle']}": workspace
-        for workspace in workspaces
-    }
+            exit_with_success(
+                f"Logged in to Prefect Cloud using profile {profile_name!r}.\n"
+                f"Workspace is currently set to {current_workspace!r}. "
+                f"The workspace can be changed using `prefect cloud workspace set`."
+            )
 
     if not workspace_handle:
         workspace_handle = select_workspace(workspace_handle_details.keys())
 
-    cloud_profile_name = app.console.input(
-        "Creating a profile for this Prefect Cloud login. Please specify a profile name: "
-    )
-
-    cloud_profile_name = cloud_profile_name.strip()
-    if cloud_profile_name == "":
-        exit_with_error("Please provide a non-empty profile name.")
+    if workspace_handle not in workspace_handle_details:
+        exit_with_error(f"Workspace {workspace_handle!r} not found.")
 
-    if cloud_profile_name in profiles:
-        exit_with_error(f"Profile {cloud_profile_name!r} already exists.")
-
-    profiles.add_profile(
-        profiles[profiles.active_name].copy(
-            update={
-                "name": cloud_profile_name,
-            }
-        )
-    )
-
-    profiles.update_profile(
-        cloud_profile_name,
+    current_profile = update_current_profile(
         {
             PREFECT_API_URL: build_url_from_workspace(
                 workspace_handle_details[workspace_handle]
@@ -235,16 +213,36 @@ async def login(
         },
     )
 
-    profiles.set_active(cloud_profile_name)
-    save_profiles(profiles)
-
     exit_with_success(
-        f"Logged in to Prefect Cloud using profile {cloud_profile_name!r}.\n"
+        f"Logged in to Prefect Cloud using profile {current_profile.name!r}.\n"
         f"Workspace is currently set to {workspace_handle!r}. "
         f"The workspace can be changed using `prefect cloud workspace set`."
     )
 
 
+@cloud_app.command()
+async def logout():
+    """
+    Logout the current workspace.
+    Reset PREFECT_API_KEY and PREFECT_API_URL to default.
+    """
+    current_profile = prefect.context.get_settings_context().profile
+    if current_profile is None:
+        exit_with_error("There is no current profile set.")
+
+    if current_profile.settings.get(PREFECT_API_KEY) is None:
+        exit_with_error("Current profile is not logged into Prefect Cloud.")
+
+    update_current_profile(
+        {
+            PREFECT_API_URL: None,
+            PREFECT_API_KEY: None,
+        },
+    )
+
+    exit_with_success("Logged out from Prefect Cloud.")
+
+
 @workspace_app.command()
 async def ls():
     """List available workspaces."""


Test patch:
diff --git a/tests/cli/test_cloud.py b/tests/cli/test_cloud.py
--- a/tests/cli/test_cloud.py
+++ b/tests/cli/test_cloud.py
@@ -5,7 +5,6 @@
 from prefect.settings import (
     PREFECT_API_KEY,
     PREFECT_API_URL,
-    PREFECT_DEBUG_MODE,
     Profile,
     ProfilesCollection,
     load_profiles,
@@ -62,7 +61,7 @@ def mock_select_workspace(workspaces):
     return list(workspaces)[0]
 
 
-def test_invalid_login(monkeypatch):
+def test_login_with_invalid_key(monkeypatch):
     monkeypatch.setattr(
         "prefect.client.cloud.get_cloud_client", get_unauthorized_mock_cloud_client
     )
@@ -89,7 +88,7 @@ def test_invalid_login(monkeypatch):
     )
 
 
-def test_login_creates_profile(monkeypatch):
+def test_login_without_workspace_handle(monkeypatch):
     monkeypatch.setattr("prefect.cli.cloud.get_cloud_client", get_mock_cloud_client)
     monkeypatch.setattr("prefect.cli.cloud.select_workspace", mock_select_workspace)
 
@@ -101,50 +100,103 @@ def test_login_creates_profile(monkeypatch):
     with use_profile(cloud_profile):
         invoke_and_assert(
             ["cloud", "login", "--key", API_KEY],
-            user_input="test-profile",
             expected_code=0,
             expected_output_contains=(
-                "Logged in to Prefect Cloud using profile 'test-profile'",
-                "Workspace is currently set to 'account-1/workspace-1'",
+                f"Logged in to Prefect Cloud using profile {cloud_profile!r}.\n"
+                f"Workspace is currently set to {FULL_HANDLE!r}. "
+                "The workspace can be changed using `prefect cloud workspace set`."
             ),
         )
 
     profiles = load_profiles()
-    assert profiles["test-profile"].settings == {
+    assert profiles[cloud_profile].settings == {
         PREFECT_API_URL: FULL_URL,
         PREFECT_API_KEY: API_KEY,
     }
 
 
-def test_login_preserves_original_profile(monkeypatch):
+def test_login_with_workspace_handle(monkeypatch):
     monkeypatch.setattr("prefect.cli.cloud.get_cloud_client", get_mock_cloud_client)
-    monkeypatch.setattr("prefect.cli.cloud.select_workspace", mock_select_workspace)
 
     cloud_profile = "cloud-foo"
     save_profiles(
-        ProfilesCollection(
-            [Profile(name=cloud_profile, settings={PREFECT_DEBUG_MODE: True})],
-            active=None,
-        )
+        ProfilesCollection([Profile(name=cloud_profile, settings={})], active=None)
     )
 
     with use_profile(cloud_profile):
         invoke_and_assert(
-            ["cloud", "login", "--key", API_KEY],
-            user_input="test-profile",
+            ["cloud", "login", "--key", API_KEY, "--workspace", FULL_HANDLE],
             expected_code=0,
             expected_output_contains=(
-                "Logged in to Prefect Cloud using profile 'test-profile'",
-                "Workspace is currently set to 'account-1/workspace-1'",
+                f"Logged in to Prefect Cloud using profile {cloud_profile!r}.\n"
+                f"Workspace is currently set to {FULL_HANDLE!r}. "
+                "The workspace can be changed using `prefect cloud workspace set`."
             ),
         )
 
     profiles = load_profiles()
     assert profiles[cloud_profile].settings == {
-        PREFECT_DEBUG_MODE: True,
+        PREFECT_API_URL: FULL_URL,
+        PREFECT_API_KEY: API_KEY,
     }
 
 
+def test_login_with_invalid_workspace_handle(monkeypatch):
+    monkeypatch.setattr("prefect.cli.cloud.get_cloud_client", get_mock_cloud_client)
+
+    cloud_profile = "cloud-foo"
+    save_profiles(
+        ProfilesCollection([Profile(name=cloud_profile, settings={})], active=None)
+    )
+
+    with use_profile(cloud_profile):
+        invoke_and_assert(
+            ["cloud", "login", "--key", API_KEY, "--workspace", "foo/bar"],
+            expected_code=1,
+            expected_output_contains="Workspace 'foo/bar' not found.",
+        )
+
+
+def test_logout_current_profile_is_not_logged():
+    cloud_profile = "cloud-foo"
+    save_profiles(
+        ProfilesCollection([Profile(name=cloud_profile, settings={})], active=None)
+    )
+
+    with use_profile(cloud_profile):
+        invoke_and_assert(
+            ["cloud", "logout"],
+            expected_code=1,
+            expected_output_contains="Current profile is not logged into Prefect Cloud.",
+        )
+
+
+def test_logout_reset_prefect_api_key_and_prefect_api_url():
+    cloud_profile = "cloud-foo"
+    save_profiles(
+        ProfilesCollection(
+            [
+                Profile(
+                    name=cloud_profile,
+                    settings={PREFECT_API_URL: FULL_URL, PREFECT_API_KEY: API_KEY},
+                )
+            ],
+            active=None,
+        )
+    )
+
+    with use_profile(cloud_profile):
+        invoke_and_assert(
+            ["cloud", "logout"],
+            expected_code=0,
+            expected_output_contains="Logged out from Prefect Cloud.",
+        )
+
+    profiles = load_profiles()
+    assert PREFECT_API_URL not in profiles[cloud_profile].settings
+    assert PREFECT_API_KEY not in profiles[cloud_profile].settings
+
+
 def test_cannot_set_workspace_if_you_are_not_logged_in():
     cloud_profile = "cloud-foo"
     save_profiles(