okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

Aly Raffauffeat(api)!: migrate daemon and clients to protocol 361c43ca

main
6.2 KiB163 linesraw
1"""Sessions at remote locations: `[user@]host:/abs/path` workspaces.
2
3A remote location is a stored key the daemon parses and labels without
4connecting to the host. These cover what a client sees: the canonical
5spelling and parsed `location`, the host label's `user@` elision against
6`ssh -G`, rejected forms, and moving between local and remote. What needs
7the host itself (kernels, `~`, folders, project files) is
8remote_kernels_test.py, over a loopback ssh.
9"""
10
11import json
12import shutil
13import subprocess
14import unittest
15import urllib.error
16import urllib.parse
17
18from harness import Albedo, exclusive, operation_id
19
20HOST = "albedo-e2e-nowhere"
21
22
23def ssh_user(host):
24    """The user ssh would pick for `host`, or None without ssh."""
25    if shutil.which("ssh") is None:
26        return None
27    output = subprocess.run(
28        ["ssh", "-G", host], capture_output=True, text=True, check=True
29    ).stdout
30    return next(
31        line.split(" ", 1)[1]
32        for line in output.splitlines()
33        if line.startswith("user ")
34    )
35
36
37class LocationsTest(unittest.TestCase):
38    def setUp(self):
39        self.app = Albedo().__enter__()
40        self.addCleanup(self.app.__exit__, None, None, None)
41
42    def api(self, path, body=None, **options):
43        with self.app.api(path, body, **options) as response:
44            return json.load(response)
45
46    def failure(self, path, body=None, **options):
47        with self.assertRaises(urllib.error.HTTPError) as caught:
48            self.api(path, body, **options)
49        return caught.exception.code, json.load(caught.exception)["detail"]
50
51    def create(self, workspace):
52        return self.api(
53            f"/sessions/{operation_id()}",
54            {
55                "kind": "new",
56                "workspace": workspace,
57                "provider_profile": self.app.profile,
58            },
59            method="PUT",
60            headers={"If-None-Match": "*"},
61        )
62
63    def move(self, session, workspace):
64        snapshot = self.api(f"/sessions/{session}?tail=0")
65        resource = snapshot["configuration_resource"]
66        return self.api(
67            resource["url"],
68            {"workspace": workspace, "family_revision": snapshot["family_revision"]},
69            method="PATCH",
70            headers={"If-Match": resource["etag"]},
71        )["session"]
72
73    def test_remote_workspace_is_stored_canonically_with_its_location(self):
74        created = self.create(f"mayer@{HOST}:/home/mayer//proj/./albedo/")
75        canonical = f"mayer@{HOST}:/home/mayer/proj/albedo"
76        self.assertEqual(created["workspace"], canonical)
77        location = created["location"]
78        self.assertEqual(
79            (location["host"], location["user"], location["path"]),
80            (HOST, "mayer", "/home/mayer/proj/albedo"),
81        )
82        query = urllib.parse.urlencode({"workspace": canonical})
83        listed = next(
84            s
85            for s in self.api("/sessions?" + query)["items"]
86            if s["id"] == created["id"]
87        )
88        self.assertEqual(listed["workspace"], canonical)
89
90        local = self.create(str(self.app.workspace))
91        self.assertEqual(local["workspace"], str(self.app.workspace))
92        self.assertEqual(
93            local["location"],
94            {
95                "host": None,
96                "user": None,
97                "path": str(self.app.workspace),
98                "label": None,
99            },
100        )
101
102    def test_host_label_drops_the_user_ssh_would_pick_anyway(self):
103        self.assertEqual(self.create(f"{HOST}:/srv")["location"]["label"], HOST)
104        other = "someone-else"
105        self.assertEqual(
106            self.create(f"{other}@{HOST}:/srv")["location"]["label"],
107            f"{other}@{HOST}",
108        )
109        default = ssh_user(HOST)
110        if default is None:
111            self.skipTest("ssh is not installed: the label keeps any user")
112        self.assertEqual(
113            self.create(f"{default}@{HOST}:/srv")["location"]["label"], HOST
114        )
115
116    # `albedo new` takes the active profile, which other tests on the shared
117    # daemon rewrite; a daemon of its own keeps that profile this fixture's.
118    # exclusive: CLI new depends on the active provider profile other fixtures rewrite
119    @exclusive
120    def test_the_cli_starts_a_remote_session_without_resolving_it_locally(self):
121        session = json.loads(self.app.cli("new", f"{HOST}:/srv/cli"))["session"]
122        listed = next(s for s in self.api("/sessions")["items"] if s["id"] == session)
123        self.assertEqual(listed["workspace"], f"{HOST}:/srv/cli")
124
125    def test_unusable_locations_are_rejected(self):
126        for workspace, said in (
127            (f"{HOST}:proj", "must be absolute"),
128            (f"{HOST}:", "must be absolute"),
129            ("-oProxyCommand=touch:/tmp", "not a valid host"),
130            (f"-l@{HOST}:/srv", "not a valid user"),
131            ("relative/path", "absolute path or host:/absolute/path"),
132            ("dir/with:colon", "absolute path or host:/absolute/path"),
133        ):
134            with self.subTest(workspace=workspace):
135                status, message = self.failure(
136                    f"/sessions/{operation_id()}",
137                    {
138                        "kind": "new",
139                        "workspace": workspace,
140                        "provider_profile": self.app.profile,
141                    },
142                    method="PUT",
143                    headers={"If-None-Match": "*"},
144                )
145                self.assertEqual(status, 400)
146                self.assertIn(said, message)
147
148    def test_a_session_moves_to_a_remote_location_and_back(self):
149        session = self.app.session()
150        moved = self.move(session, f"{HOST}:/srv//app/")
151        self.assertEqual(moved["workspace"], f"{HOST}:/srv/app")
152        self.assertEqual(moved["location"]["host"], HOST)
153        with self.assertRaises(urllib.error.HTTPError) as invalid:
154            self.move(session, f"{HOST}:app")
155        self.assertEqual(invalid.exception.code, 400)
156        self.assertIn("must be absolute", json.load(invalid.exception)["detail"])
157        back = self.move(session, str(self.app.workspace))
158        self.assertEqual(back["workspace"], str(self.app.workspace))
159        self.assertIsNone(back["location"]["host"])
160
161
162if __name__ == "__main__":
163    unittest.main()