runtime-sdk 0.4.49__py3-none-win_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- runtime_sdk/__init__.py +18 -0
- runtime_sdk/account.py +243 -0
- runtime_sdk/arguments.py +10 -0
- runtime_sdk/auth.py +175 -0
- runtime_sdk/cli.py +985 -0
- runtime_sdk/client.py +616 -0
- runtime_sdk/completion.py +348 -0
- runtime_sdk/computers.py +578 -0
- runtime_sdk/config.py +201 -0
- runtime_sdk/console.py +118 -0
- runtime_sdk/files.py +233 -0
- runtime_sdk/github.py +229 -0
- runtime_sdk/goals.py +368 -0
- runtime_sdk/output.py +69 -0
- runtime_sdk/providers.py +422 -0
- runtime_sdk/proxy.py +600 -0
- runtime_sdk/runtime-tui.exe +0 -0
- runtime_sdk/secrets.py +155 -0
- runtime_sdk/services.py +173 -0
- runtime_sdk/ssh.py +197 -0
- runtime_sdk/terminal.py +330 -0
- runtime_sdk-0.4.49.dist-info/METADATA +273 -0
- runtime_sdk-0.4.49.dist-info/RECORD +26 -0
- runtime_sdk-0.4.49.dist-info/WHEEL +5 -0
- runtime_sdk-0.4.49.dist-info/entry_points.txt +2 -0
- runtime_sdk-0.4.49.dist-info/top_level.txt +1 -0
runtime_sdk/secrets.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from runtime_sdk import output as cli_output
|
|
6
|
+
from runtime_sdk.auth import authenticated_client
|
|
7
|
+
from runtime_sdk.config import RuntimeConfig, RuntimeConfigError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _coerce_secret_set_slug(slug: str | None) -> str | None:
|
|
11
|
+
value = str(slug or "").strip()
|
|
12
|
+
return value or None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _parse_secret_pairs(pairs: list[str] | None) -> dict[str, str]:
|
|
16
|
+
values: dict[str, str] = {}
|
|
17
|
+
for pair in pairs or []:
|
|
18
|
+
if "=" not in pair:
|
|
19
|
+
raise RuntimeConfigError(f"invalid secret pair: {pair}")
|
|
20
|
+
key, value = pair.split("=", 1)
|
|
21
|
+
key = key.strip()
|
|
22
|
+
if not key:
|
|
23
|
+
raise RuntimeConfigError(f"invalid secret pair: {pair}")
|
|
24
|
+
values[key] = value
|
|
25
|
+
return values
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _parse_secret_delivery(
|
|
29
|
+
proxy_specs: list[str] | None, values: dict[str, str]
|
|
30
|
+
) -> dict[str, dict[str, str]]:
|
|
31
|
+
delivery: dict[str, dict[str, str]] = {}
|
|
32
|
+
for spec in proxy_specs or []:
|
|
33
|
+
if "=" not in spec:
|
|
34
|
+
raise RuntimeConfigError(f"invalid proxy delivery: {spec}")
|
|
35
|
+
key, host = spec.split("=", 1)
|
|
36
|
+
key = key.strip()
|
|
37
|
+
host = host.strip()
|
|
38
|
+
if not key or not host or key not in values:
|
|
39
|
+
raise RuntimeConfigError(f"invalid proxy delivery: {spec}")
|
|
40
|
+
delivery[key] = {
|
|
41
|
+
"delivery": "proxy",
|
|
42
|
+
"target_host": host,
|
|
43
|
+
"header": "Authorization",
|
|
44
|
+
"scheme": "Bearer",
|
|
45
|
+
}
|
|
46
|
+
return delivery
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _parse_env_file(path: str) -> dict[str, str]:
|
|
50
|
+
try:
|
|
51
|
+
lines = Path(path).read_text(encoding="utf-8").splitlines()
|
|
52
|
+
except OSError as exc:
|
|
53
|
+
raise RuntimeConfigError(f"failed to read env file: {exc}") from exc
|
|
54
|
+
values: dict[str, str] = {}
|
|
55
|
+
for raw_line in lines:
|
|
56
|
+
line = raw_line.strip()
|
|
57
|
+
if not line or line.startswith("#"):
|
|
58
|
+
continue
|
|
59
|
+
if line.startswith("export "):
|
|
60
|
+
line = line[len("export ") :].strip()
|
|
61
|
+
if "=" not in line:
|
|
62
|
+
raise RuntimeConfigError(f"invalid env line: {raw_line}")
|
|
63
|
+
key, value = line.split("=", 1)
|
|
64
|
+
key = key.strip()
|
|
65
|
+
if not key:
|
|
66
|
+
raise RuntimeConfigError(f"invalid env line: {raw_line}")
|
|
67
|
+
value = value.strip()
|
|
68
|
+
if value and value[0] == value[-1] and value[0] in {'"', "'"}:
|
|
69
|
+
value = value[1:-1]
|
|
70
|
+
values[key] = value
|
|
71
|
+
return values
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def handle_secrets_list(config: RuntimeConfig) -> int:
|
|
75
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
76
|
+
return err
|
|
77
|
+
client = authenticated_client(config)
|
|
78
|
+
return cli_output.report_success(client.list_secret_sets())
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def handle_secrets_show(config: RuntimeConfig, slug: str | None) -> int:
|
|
82
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
83
|
+
return err
|
|
84
|
+
resolved_slug = _coerce_secret_set_slug(slug)
|
|
85
|
+
if resolved_slug is None:
|
|
86
|
+
if not cli_output.interactive():
|
|
87
|
+
return cli_output.report_error("secret set slug is required")
|
|
88
|
+
resolved_slug = cli_output.prompt_text("Secret set slug")
|
|
89
|
+
if not resolved_slug:
|
|
90
|
+
return cli_output.report_error("secret set slug is required")
|
|
91
|
+
client = authenticated_client(config)
|
|
92
|
+
return cli_output.report_success(client.get_secret_set(resolved_slug))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def handle_secrets_set(
|
|
96
|
+
config: RuntimeConfig,
|
|
97
|
+
slug: str | None,
|
|
98
|
+
pairs: list[str] | None,
|
|
99
|
+
proxy_specs: list[str] | None,
|
|
100
|
+
) -> int:
|
|
101
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
102
|
+
return err
|
|
103
|
+
resolved_slug = _coerce_secret_set_slug(slug)
|
|
104
|
+
if resolved_slug is None:
|
|
105
|
+
if not cli_output.interactive():
|
|
106
|
+
return cli_output.report_error("secret set slug is required")
|
|
107
|
+
resolved_slug = cli_output.prompt_text("Secret set slug")
|
|
108
|
+
if not resolved_slug:
|
|
109
|
+
return cli_output.report_error("secret set slug is required")
|
|
110
|
+
values = _parse_secret_pairs(pairs)
|
|
111
|
+
if not values:
|
|
112
|
+
return cli_output.report_error("at least one KEY=VALUE pair is required")
|
|
113
|
+
delivery = _parse_secret_delivery(proxy_specs, values)
|
|
114
|
+
client = authenticated_client(config)
|
|
115
|
+
secret_set = client.put_secret_set(resolved_slug, values, delivery)
|
|
116
|
+
return cli_output.report_success({"secret_set": secret_set})
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def handle_secrets_import(
|
|
120
|
+
config: RuntimeConfig,
|
|
121
|
+
slug: str | None,
|
|
122
|
+
env_file: str,
|
|
123
|
+
proxy_specs: list[str] | None,
|
|
124
|
+
) -> int:
|
|
125
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
126
|
+
return err
|
|
127
|
+
resolved_slug = _coerce_secret_set_slug(slug)
|
|
128
|
+
if resolved_slug is None:
|
|
129
|
+
if not cli_output.interactive():
|
|
130
|
+
return cli_output.report_error("secret set slug is required")
|
|
131
|
+
resolved_slug = cli_output.prompt_text("Secret set slug")
|
|
132
|
+
if not resolved_slug:
|
|
133
|
+
return cli_output.report_error("secret set slug is required")
|
|
134
|
+
values = _parse_env_file(env_file)
|
|
135
|
+
if not values:
|
|
136
|
+
return cli_output.report_error("env file did not contain any secrets")
|
|
137
|
+
delivery = _parse_secret_delivery(proxy_specs, values)
|
|
138
|
+
client = authenticated_client(config)
|
|
139
|
+
secret_set = client.put_secret_set(resolved_slug, values, delivery)
|
|
140
|
+
return cli_output.report_success({"secret_set": secret_set})
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def handle_secrets_delete(config: RuntimeConfig, slug: str | None) -> int:
|
|
144
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
145
|
+
return err
|
|
146
|
+
resolved_slug = _coerce_secret_set_slug(slug)
|
|
147
|
+
if resolved_slug is None:
|
|
148
|
+
if not cli_output.interactive():
|
|
149
|
+
return cli_output.report_error("secret set slug is required")
|
|
150
|
+
resolved_slug = cli_output.prompt_text("Secret set slug")
|
|
151
|
+
if not resolved_slug:
|
|
152
|
+
return cli_output.report_error("secret set slug is required")
|
|
153
|
+
client = authenticated_client(config)
|
|
154
|
+
client.delete_secret_set(resolved_slug)
|
|
155
|
+
return cli_output.report_success({"slug": resolved_slug, "deleted": True})
|
runtime_sdk/services.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from runtime_sdk import arguments, computers, output as cli_output
|
|
7
|
+
from runtime_sdk.auth import authenticated_client
|
|
8
|
+
from runtime_sdk.config import RuntimeConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def handle_startup_show(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
12
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
13
|
+
return err
|
|
14
|
+
|
|
15
|
+
if computer_id is None:
|
|
16
|
+
if not cli_output.interactive():
|
|
17
|
+
return cli_output.report_error("computer id is required")
|
|
18
|
+
picked = computers.pick_computer(config, "Pick a computer")
|
|
19
|
+
if picked is None:
|
|
20
|
+
return 0
|
|
21
|
+
computer_id = picked.get("id") or picked.get("slug")
|
|
22
|
+
if not computer_id:
|
|
23
|
+
return cli_output.report_error("computer is missing an id")
|
|
24
|
+
|
|
25
|
+
client = authenticated_client(config)
|
|
26
|
+
computer_id = computers.resolve_computer_ref(client, computer_id)
|
|
27
|
+
result = client.get_startup_config(computer_id)
|
|
28
|
+
return cli_output.report_success(result)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def handle_startup_set(
|
|
32
|
+
config: RuntimeConfig,
|
|
33
|
+
computer_id: str | None,
|
|
34
|
+
command: str | None,
|
|
35
|
+
cwd: str | None,
|
|
36
|
+
port: int | None,
|
|
37
|
+
) -> int:
|
|
38
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
39
|
+
return err
|
|
40
|
+
if not computer_id:
|
|
41
|
+
return cli_output.report_error("computer id is required")
|
|
42
|
+
if not command:
|
|
43
|
+
return cli_output.report_error("startup command is required")
|
|
44
|
+
if port is None:
|
|
45
|
+
return cli_output.report_error("port is required")
|
|
46
|
+
if port <= 0 or port > 65535:
|
|
47
|
+
return cli_output.report_error("port must be between 1 and 65535")
|
|
48
|
+
|
|
49
|
+
client = authenticated_client(config)
|
|
50
|
+
computer_id = computers.resolve_computer_ref(client, computer_id)
|
|
51
|
+
result = client.set_startup_config(computer_id, command=command, cwd=cwd, port=port)
|
|
52
|
+
return cli_output.report_success(result)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def handle_startup_clear(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
56
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
57
|
+
return err
|
|
58
|
+
if not computer_id:
|
|
59
|
+
return cli_output.report_error("computer id is required")
|
|
60
|
+
|
|
61
|
+
client = authenticated_client(config)
|
|
62
|
+
computer_id = computers.resolve_computer_ref(client, computer_id)
|
|
63
|
+
result = client.clear_startup_config(computer_id)
|
|
64
|
+
|
|
65
|
+
return cli_output.report_success(result or {"configured": False})
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def handle_service_run(
|
|
69
|
+
config: RuntimeConfig,
|
|
70
|
+
computer_id: str | None,
|
|
71
|
+
port: int | None,
|
|
72
|
+
cwd: str | None,
|
|
73
|
+
command_parts: list[str] | None,
|
|
74
|
+
) -> int:
|
|
75
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
76
|
+
return err
|
|
77
|
+
if not computer_id:
|
|
78
|
+
return cli_output.report_error("computer id is required")
|
|
79
|
+
if port is None or port <= 0 or port > 65535:
|
|
80
|
+
return cli_output.report_error("port must be between 1 and 65535")
|
|
81
|
+
command = arguments.command_from_remainder(command_parts)
|
|
82
|
+
if not command:
|
|
83
|
+
return cli_output.report_error("service run requires -- <command...>")
|
|
84
|
+
|
|
85
|
+
client = authenticated_client(config)
|
|
86
|
+
computer_id = computers.resolve_computer_ref(client, computer_id)
|
|
87
|
+
result = client.run_service(computer_id, command=command, cwd=cwd, port=port)
|
|
88
|
+
return cli_output.report_success(result)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def handle_service_show(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
92
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
93
|
+
return err
|
|
94
|
+
|
|
95
|
+
if computer_id is None:
|
|
96
|
+
if not cli_output.interactive():
|
|
97
|
+
return cli_output.report_error("computer id is required")
|
|
98
|
+
picked = computers.pick_computer(config, "Pick a computer")
|
|
99
|
+
if picked is None:
|
|
100
|
+
return 0
|
|
101
|
+
computer_id = picked.get("id") or picked.get("slug")
|
|
102
|
+
if not computer_id:
|
|
103
|
+
return cli_output.report_error("computer is missing an id")
|
|
104
|
+
|
|
105
|
+
client = authenticated_client(config)
|
|
106
|
+
computer_id = computers.resolve_computer_ref(client, computer_id)
|
|
107
|
+
result = client.get_service_config(computer_id)
|
|
108
|
+
return cli_output.report_success(result)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def handle_service_logs(
|
|
112
|
+
config: RuntimeConfig, computer_id: str | None, follow: bool
|
|
113
|
+
) -> int:
|
|
114
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
115
|
+
return err
|
|
116
|
+
if not computer_id:
|
|
117
|
+
return cli_output.report_error("computer id is required")
|
|
118
|
+
client = authenticated_client(config)
|
|
119
|
+
computer_id = computers.resolve_computer_ref(client, computer_id)
|
|
120
|
+
for line in client.stream_service_logs(computer_id, follow=follow):
|
|
121
|
+
try:
|
|
122
|
+
frame = json.loads(line)
|
|
123
|
+
except ValueError:
|
|
124
|
+
print(line)
|
|
125
|
+
continue
|
|
126
|
+
stdout = frame.get("stdout") or ""
|
|
127
|
+
stderr = frame.get("stderr") or ""
|
|
128
|
+
data = frame.get("data") or ""
|
|
129
|
+
error = frame.get("error") or ""
|
|
130
|
+
if stdout:
|
|
131
|
+
print(stdout, end="")
|
|
132
|
+
if stderr:
|
|
133
|
+
print(stderr, end="", file=sys.stderr)
|
|
134
|
+
if data:
|
|
135
|
+
print(data, end="")
|
|
136
|
+
if error:
|
|
137
|
+
print(error, file=sys.stderr)
|
|
138
|
+
return 0
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def handle_service_restart(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
142
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
143
|
+
return err
|
|
144
|
+
if not computer_id:
|
|
145
|
+
return cli_output.report_error("computer id is required")
|
|
146
|
+
client = authenticated_client(config)
|
|
147
|
+
computer_id = computers.resolve_computer_ref(client, computer_id)
|
|
148
|
+
result = client.restart_service(computer_id)
|
|
149
|
+
return cli_output.report_success(result)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def handle_service_stop(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
153
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
154
|
+
return err
|
|
155
|
+
if not computer_id:
|
|
156
|
+
return cli_output.report_error("computer id is required")
|
|
157
|
+
client = authenticated_client(config)
|
|
158
|
+
computer_id = computers.resolve_computer_ref(client, computer_id)
|
|
159
|
+
result = client.stop_service(computer_id)
|
|
160
|
+
return cli_output.report_success(result)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def handle_service_clear(config: RuntimeConfig, computer_id: str | None) -> int:
|
|
164
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
165
|
+
return err
|
|
166
|
+
if not computer_id:
|
|
167
|
+
return cli_output.report_error("computer id is required")
|
|
168
|
+
|
|
169
|
+
client = authenticated_client(config)
|
|
170
|
+
computer_id = computers.resolve_computer_ref(client, computer_id)
|
|
171
|
+
result = client.clear_service_config(computer_id)
|
|
172
|
+
|
|
173
|
+
return cli_output.report_success(result or {"configured": False})
|
runtime_sdk/ssh.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import secrets
|
|
5
|
+
import shlex
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Callable
|
|
12
|
+
|
|
13
|
+
from runtime_sdk import computers, output as cli_output, proxy
|
|
14
|
+
from runtime_sdk.auth import authenticated_client
|
|
15
|
+
from runtime_sdk.client import RuntimeAPIError, RuntimeClient
|
|
16
|
+
from runtime_sdk.config import RuntimeConfig, RuntimeConfigError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def handle_ssh(
|
|
20
|
+
config: RuntimeConfig, computer_id: str | None, ssh_args: list[str] | None = None
|
|
21
|
+
) -> int:
|
|
22
|
+
if (err := cli_output.require_auth(config)) is not None:
|
|
23
|
+
return err
|
|
24
|
+
ssh_bin = shutil.which("ssh")
|
|
25
|
+
if not ssh_bin:
|
|
26
|
+
return cli_output.report_error("ssh is not installed")
|
|
27
|
+
ssh_keygen_bin = shutil.which("ssh-keygen")
|
|
28
|
+
if not ssh_keygen_bin:
|
|
29
|
+
return cli_output.report_error("ssh-keygen is not installed")
|
|
30
|
+
|
|
31
|
+
if computer_id is None:
|
|
32
|
+
if not cli_output.interactive():
|
|
33
|
+
return cli_output.report_error("computer id is required")
|
|
34
|
+
picked = computers.pick_computer(
|
|
35
|
+
config,
|
|
36
|
+
"Pick a computer to ssh",
|
|
37
|
+
lambda c: computers.computer_internal_status(c) in ("running", "cold"),
|
|
38
|
+
)
|
|
39
|
+
if picked is None:
|
|
40
|
+
return 0
|
|
41
|
+
computer_id = picked.get("id") or picked.get("slug")
|
|
42
|
+
if not computer_id:
|
|
43
|
+
return cli_output.report_error("computer is missing an id")
|
|
44
|
+
|
|
45
|
+
client = authenticated_client(config)
|
|
46
|
+
resolved_id = computers.resolve_computer_ref(client, str(computer_id))
|
|
47
|
+
args = _ssh_extra_args(ssh_args or [])
|
|
48
|
+
|
|
49
|
+
with tempfile.TemporaryDirectory(prefix="runtime-ssh-") as temp_dir:
|
|
50
|
+
temp = Path(temp_dir)
|
|
51
|
+
key_path = temp / "id_ed25519"
|
|
52
|
+
known_hosts = temp / "known_hosts"
|
|
53
|
+
marker = "runtimevm-ssh-" + secrets.token_hex(8)
|
|
54
|
+
if (key_status := _generate_ssh_key(ssh_keygen_bin, key_path)) != 0:
|
|
55
|
+
return cli_output.report_error(
|
|
56
|
+
f"ssh-keygen failed with exit code {key_status}"
|
|
57
|
+
)
|
|
58
|
+
public_key = (
|
|
59
|
+
key_path.with_suffix(key_path.suffix + ".pub")
|
|
60
|
+
.read_text(encoding="utf-8")
|
|
61
|
+
.strip()
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
client.create_terminal_ticket(resolved_id)
|
|
65
|
+
cleanup_proxy: Callable[[], None] | None = None
|
|
66
|
+
try:
|
|
67
|
+
_install_temporary_ssh_key(client, resolved_id, public_key, marker)
|
|
68
|
+
local_port, cleanup_proxy = proxy.start_temporary_proxy(
|
|
69
|
+
config, resolved_id, 22
|
|
70
|
+
)
|
|
71
|
+
ssh_cmd = [
|
|
72
|
+
ssh_bin,
|
|
73
|
+
"-i",
|
|
74
|
+
str(key_path),
|
|
75
|
+
"-p",
|
|
76
|
+
str(local_port),
|
|
77
|
+
"-o",
|
|
78
|
+
"IdentitiesOnly=yes",
|
|
79
|
+
"-o",
|
|
80
|
+
f"UserKnownHostsFile={known_hosts}",
|
|
81
|
+
"-o",
|
|
82
|
+
"StrictHostKeyChecking=accept-new",
|
|
83
|
+
"-o",
|
|
84
|
+
"LogLevel=ERROR",
|
|
85
|
+
"runtime@127.0.0.1",
|
|
86
|
+
]
|
|
87
|
+
ssh_cmd.extend(args)
|
|
88
|
+
return subprocess.run(ssh_cmd).returncode
|
|
89
|
+
finally:
|
|
90
|
+
if cleanup_proxy is not None:
|
|
91
|
+
cleanup_proxy()
|
|
92
|
+
_remove_temporary_ssh_key(client, resolved_id, marker)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _ssh_extra_args(args: list[str]) -> list[str]:
|
|
96
|
+
if args and args[0] == "--":
|
|
97
|
+
return args[1:]
|
|
98
|
+
return args
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _generate_ssh_key(ssh_keygen_bin: str, key_path: Path) -> int:
|
|
102
|
+
return subprocess.run(
|
|
103
|
+
[
|
|
104
|
+
ssh_keygen_bin,
|
|
105
|
+
"-q",
|
|
106
|
+
"-t",
|
|
107
|
+
"ed25519",
|
|
108
|
+
"-N",
|
|
109
|
+
"",
|
|
110
|
+
"-C",
|
|
111
|
+
"runtimevm-ephemeral",
|
|
112
|
+
"-f",
|
|
113
|
+
str(key_path),
|
|
114
|
+
]
|
|
115
|
+
).returncode
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _install_temporary_ssh_key(
|
|
119
|
+
client: RuntimeClient, computer_id: str, public_key: str, marker: str
|
|
120
|
+
) -> None:
|
|
121
|
+
encoded_key = base64.b64encode(public_key.encode()).decode()
|
|
122
|
+
script = f"""set -euo pipefail
|
|
123
|
+
if ! command -v sshd >/dev/null 2>&1 && [ ! -x /usr/sbin/sshd ]; then
|
|
124
|
+
echo "openssh-server is not installed in this VM image" >&2
|
|
125
|
+
exit 127
|
|
126
|
+
fi
|
|
127
|
+
mkdir -p /run/sshd
|
|
128
|
+
if command -v systemctl >/dev/null 2>&1; then
|
|
129
|
+
systemctl start ssh 2>/dev/null || systemctl start sshd 2>/dev/null || /usr/sbin/sshd
|
|
130
|
+
else
|
|
131
|
+
/usr/sbin/sshd
|
|
132
|
+
fi
|
|
133
|
+
install -d -m 755 -o runtime -g runtime /home/runtime/.runtime
|
|
134
|
+
cat > /home/runtime/.runtime/ssh-session <<'RUNTIME_SSH_SESSION'
|
|
135
|
+
#!/bin/bash
|
|
136
|
+
set -e
|
|
137
|
+
if [ -f /home/runtime/.runtime/prompt.sh ]; then
|
|
138
|
+
. /home/runtime/.runtime/prompt.sh
|
|
139
|
+
fi
|
|
140
|
+
if [ -f /home/runtime/.runtime/branch-workspace.sh ]; then
|
|
141
|
+
. /home/runtime/.runtime/branch-workspace.sh
|
|
142
|
+
fi
|
|
143
|
+
if [ -n "${{SSH_ORIGINAL_COMMAND:-}}" ]; then
|
|
144
|
+
exec /bin/bash -lc "$SSH_ORIGINAL_COMMAND"
|
|
145
|
+
fi
|
|
146
|
+
exec /bin/bash -l
|
|
147
|
+
RUNTIME_SSH_SESSION
|
|
148
|
+
chown runtime:runtime /home/runtime/.runtime/ssh-session
|
|
149
|
+
chmod 755 /home/runtime/.runtime/ssh-session
|
|
150
|
+
install -d -m 700 -o runtime -g runtime /home/runtime/.ssh
|
|
151
|
+
auth=/home/runtime/.ssh/authorized_keys
|
|
152
|
+
touch "$auth"
|
|
153
|
+
chown runtime:runtime "$auth"
|
|
154
|
+
chmod 600 "$auth"
|
|
155
|
+
tmp="$(mktemp)"
|
|
156
|
+
grep -Fv -- {shlex.quote(marker)} "$auth" > "$tmp" || true
|
|
157
|
+
cat "$tmp" > "$auth"
|
|
158
|
+
rm -f "$tmp"
|
|
159
|
+
key="$(printf %s {shlex.quote(encoded_key)} | base64 -d)"
|
|
160
|
+
printf 'command="/home/runtime/.runtime/ssh-session" %s %s\\n' "$key" {shlex.quote(marker)} >> "$auth"
|
|
161
|
+
chown runtime:runtime "$auth"
|
|
162
|
+
chmod 600 "$auth"
|
|
163
|
+
"""
|
|
164
|
+
result = client.run_command(
|
|
165
|
+
computer_id, script, uid=0, gid=0, cwd="/", shell="/bin/bash"
|
|
166
|
+
)
|
|
167
|
+
if int(result.get("exit_code") or 0) != 0:
|
|
168
|
+
stderr = str(result.get("stderr") or "").strip()
|
|
169
|
+
raise RuntimeConfigError(stderr or "failed to install temporary SSH key")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _remove_temporary_ssh_key(
|
|
173
|
+
client: RuntimeClient, computer_id: str, marker: str
|
|
174
|
+
) -> None:
|
|
175
|
+
script = f"""set -euo pipefail
|
|
176
|
+
auth=/home/runtime/.ssh/authorized_keys
|
|
177
|
+
[ -f "$auth" ] || exit 0
|
|
178
|
+
tmp="$(mktemp)"
|
|
179
|
+
grep -Fv -- {shlex.quote(marker)} "$auth" > "$tmp" || true
|
|
180
|
+
cat "$tmp" > "$auth"
|
|
181
|
+
rm -f "$tmp"
|
|
182
|
+
chown runtime:runtime "$auth"
|
|
183
|
+
chmod 600 "$auth"
|
|
184
|
+
"""
|
|
185
|
+
try:
|
|
186
|
+
result = client.run_command(
|
|
187
|
+
computer_id, script, uid=0, gid=0, cwd="/", shell="/bin/bash"
|
|
188
|
+
)
|
|
189
|
+
except RuntimeAPIError as exc:
|
|
190
|
+
print(f"warning: failed to remove temporary SSH key: {exc}", file=sys.stderr)
|
|
191
|
+
return
|
|
192
|
+
if int(result.get("exit_code") or 0) != 0:
|
|
193
|
+
stderr = str(result.get("stderr") or "").strip()
|
|
194
|
+
print(
|
|
195
|
+
f"warning: failed to remove temporary SSH key: {stderr or 'cleanup command failed'}",
|
|
196
|
+
file=sys.stderr,
|
|
197
|
+
)
|