snapctl 0.48.1__py3-none-any.whl → 0.49.1__py3-none-any.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.
Potentially problematic release.
This version of snapctl might be problematic. Click here for more details.
- snapctl/commands/byosnap.py +23 -12
- snapctl/commands/byows.py +401 -0
- snapctl/commands/game.py +1 -1
- snapctl/commands/release_notes.py +12 -18
- snapctl/commands/snapend.py +11 -9
- snapctl/config/constants.py +5 -2
- snapctl/config/endpoints.py +5 -0
- snapctl/config/hashes.py +5 -1
- snapctl/data/__init__.py +0 -0
- snapctl/data/profiles/__init__.py +0 -0
- snapctl/data/releases/__init__.py +0 -0
- snapctl/data/releases/beta-0.49.0.mdx +9 -0
- snapctl/data/releases/beta-0.49.1.mdx +6 -0
- snapctl/main.py +67 -1
- snapctl/utils/helper.py +9 -0
- {snapctl-0.48.1.dist-info → snapctl-0.49.1.dist-info}/METADATA +150 -94
- {snapctl-0.48.1.dist-info → snapctl-0.49.1.dist-info}/RECORD +20 -14
- {snapctl-0.48.1.dist-info → snapctl-0.49.1.dist-info}/LICENSE +0 -0
- {snapctl-0.48.1.dist-info → snapctl-0.49.1.dist-info}/WHEEL +0 -0
- {snapctl-0.48.1.dist-info → snapctl-0.49.1.dist-info}/entry_points.txt +0 -0
snapctl/commands/byosnap.py
CHANGED
|
@@ -11,10 +11,10 @@ import subprocess
|
|
|
11
11
|
import platform as sys_platform
|
|
12
12
|
from sys import platform
|
|
13
13
|
from typing import Union, List
|
|
14
|
+
import importlib.resources as pkg_resources
|
|
14
15
|
import requests
|
|
15
16
|
from requests.exceptions import RequestException
|
|
16
17
|
import yaml
|
|
17
|
-
|
|
18
18
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
19
19
|
from snapctl.commands.snapend import Snapend
|
|
20
20
|
from snapctl.config.constants import SERVER_CALL_TIMEOUT
|
|
@@ -29,10 +29,11 @@ from snapctl.config.constants import HTTP_ERROR_SERVICE_VERSION_EXISTS, \
|
|
|
29
29
|
SNAPCTL_BYOSNAP_UPDATE_VERSION_ERROR, SNAPCTL_BYOSNAP_UPDATE_VERSION_SERVICE_IN_USE_ERROR, \
|
|
30
30
|
SNAPCTL_BYOSNAP_UPDATE_VERSION_TAG_ERROR, SNAPCTL_BYOSNAP_NOT_FOUND, \
|
|
31
31
|
HTTP_ERROR_RESOURCE_NOT_FOUND, SNAPCTL_BYOSNAP_PUBLISH_ERROR, \
|
|
32
|
-
SNAPCTL_BYOSNAP_GENERATE_PROFILE_ERROR, SNAPCTL_CONFIGURATION_INCORRECT
|
|
32
|
+
SNAPCTL_BYOSNAP_GENERATE_PROFILE_ERROR, SNAPCTL_CONFIGURATION_INCORRECT
|
|
33
33
|
from snapctl.utils.echo import info, warning, success
|
|
34
34
|
from snapctl.utils.helper import get_composite_token, snapctl_error, snapctl_success, \
|
|
35
35
|
check_dockerfile_architecture, check_use_containerd_snapshotter
|
|
36
|
+
import snapctl.data.profiles
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
class ByoSnap:
|
|
@@ -52,8 +53,8 @@ class ByoSnap:
|
|
|
52
53
|
TO_DEPRECATE_SUBCOMMANDS = [
|
|
53
54
|
'create', 'publish-image', 'publish-version', 'update-version']
|
|
54
55
|
DEFAULT_PROFILE_NAME_JSON = 'snapser-byosnap-profile.json'
|
|
56
|
+
DEFAULT_PROFILE_NAME_YML = 'snapser-byosnap-profile.yml'
|
|
55
57
|
DEFAULT_PROFILE_NAME_YAML = 'snapser-byosnap-profile.yaml'
|
|
56
|
-
PROFILE_FILES_PATH = 'snapctl/data/profiles/'
|
|
57
58
|
PROFILE_FORMATS = ['json', 'yaml', 'yml']
|
|
58
59
|
PLATFORMS = ['linux/arm64', 'linux/amd64']
|
|
59
60
|
LANGUAGES = ['go', 'node', 'python', 'java', 'csharp', 'cpp', 'rust',
|
|
@@ -501,13 +502,23 @@ class ByoSnap:
|
|
|
501
502
|
)
|
|
502
503
|
|
|
503
504
|
@staticmethod
|
|
504
|
-
def _handle_output_file(
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
505
|
+
def _handle_output_file(resource_filename: str, output_filepath: str) -> bool:
|
|
506
|
+
try:
|
|
507
|
+
with pkg_resources.open_text(snapctl.data.profiles, resource_filename) as in_file, open(output_filepath, 'w') as outfile:
|
|
508
|
+
for line in in_file:
|
|
509
|
+
outfile.write(line)
|
|
510
|
+
return True
|
|
511
|
+
except FileNotFoundError:
|
|
512
|
+
print(f"[ERROR] Could not find profile file: {resource_filename}")
|
|
513
|
+
return False
|
|
514
|
+
# @staticmethod
|
|
515
|
+
# def _handle_output_file(input_filepath, output_filepath) -> bool:
|
|
516
|
+
# file_written = False
|
|
517
|
+
# with open(input_filepath, 'r') as in_file, open(output_filepath, 'w') as outfile:
|
|
518
|
+
# for line in in_file:
|
|
519
|
+
# outfile.write(line)
|
|
520
|
+
# file_written = True
|
|
521
|
+
# return file_written
|
|
511
522
|
|
|
512
523
|
def _get_profile_contents(self) -> dict:
|
|
513
524
|
"""
|
|
@@ -1574,9 +1585,9 @@ class ByoSnap:
|
|
|
1574
1585
|
file_save_path = os.path.join(
|
|
1575
1586
|
os.getcwd(), self.profile_filename)
|
|
1576
1587
|
extension = self.profile_filename.split('.')[-1]
|
|
1588
|
+
resource_filename = f"snapser-byosnap-profile.{extension}"
|
|
1577
1589
|
file_written = ByoSnap._handle_output_file(
|
|
1578
|
-
|
|
1579
|
-
)
|
|
1590
|
+
resource_filename, file_save_path)
|
|
1580
1591
|
if file_written:
|
|
1581
1592
|
snapctl_success(
|
|
1582
1593
|
message="BYOSNAP Profile generation successful. " +
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BYOWs CLI commands
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import platform
|
|
6
|
+
import subprocess
|
|
7
|
+
import shutil
|
|
8
|
+
import time
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Union
|
|
12
|
+
import threading
|
|
13
|
+
import requests
|
|
14
|
+
from requests.exceptions import RequestException, HTTPError
|
|
15
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
16
|
+
from snapctl.config.constants import SERVER_CALL_TIMEOUT, SNAPCTL_INPUT_ERROR, \
|
|
17
|
+
SNAPCTL_BYOWS_GENERIC_ERROR, SNAPCTL_DEPENDENCY_MISSING
|
|
18
|
+
from snapctl.utils.helper import snapctl_error, snapctl_success, get_dot_snapser_dir
|
|
19
|
+
from snapctl.utils.echo import info, warning
|
|
20
|
+
|
|
21
|
+
class Byows:
|
|
22
|
+
"""
|
|
23
|
+
CLI commands exposed for a Bring your own Workstation
|
|
24
|
+
"""
|
|
25
|
+
SUBCOMMANDS = ['attach']
|
|
26
|
+
SSH_FILE = 'id_snapser_byows_attach_ed25519'
|
|
27
|
+
PORT_FORWARD_TTL = 3600 * 24 * 7
|
|
28
|
+
BYOWS_ENV_FILE = 'byows_env_setup'
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self, *, subcommand: str, base_url: str, base_snapend_url: str, api_key: Union[str, None],
|
|
32
|
+
snapend_id: Union[str, None] = None, byosnap_id: Union[str, None] = None,
|
|
33
|
+
http_port: Union[int, None] = None, grpc_port: Union[int, None] = None,
|
|
34
|
+
|
|
35
|
+
) -> None:
|
|
36
|
+
self.subcommand: str = subcommand
|
|
37
|
+
self.base_url: str = base_url
|
|
38
|
+
self.base_snapend_url: str = base_snapend_url
|
|
39
|
+
self.api_key: Union[str, None] = api_key
|
|
40
|
+
self.snapend_id: Union[str, None] = snapend_id
|
|
41
|
+
self.byosnap_id: Union[str, None] = byosnap_id
|
|
42
|
+
self.http_port: Union[int, None] = http_port
|
|
43
|
+
self.grpc_port: Union[int, None] = grpc_port
|
|
44
|
+
self.validate_input()
|
|
45
|
+
|
|
46
|
+
def validate_input(self) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Validator
|
|
49
|
+
"""
|
|
50
|
+
# Check API Key and Base URL
|
|
51
|
+
if not self.api_key or self.base_url == '':
|
|
52
|
+
snapctl_error(
|
|
53
|
+
message="Missing API Key.", code=SNAPCTL_INPUT_ERROR)
|
|
54
|
+
# Check subcommand
|
|
55
|
+
if not self.subcommand in Byows.SUBCOMMANDS:
|
|
56
|
+
snapctl_error(
|
|
57
|
+
message="Invalid command. Valid commands are " +
|
|
58
|
+
f"{', '.join(Byows.SUBCOMMANDS)}.",
|
|
59
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
60
|
+
# Check sdk-download commands
|
|
61
|
+
if self.subcommand == 'attach':
|
|
62
|
+
if not shutil.which("ssh"):
|
|
63
|
+
snapctl_error("ssh is not installed or not in PATH",
|
|
64
|
+
SNAPCTL_DEPENDENCY_MISSING)
|
|
65
|
+
if self.snapend_id is None or self.snapend_id == '':
|
|
66
|
+
snapctl_error(
|
|
67
|
+
message="Missing Input --snapend-id=$your_snapend_id",
|
|
68
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
69
|
+
if self.byosnap_id is None or self.byosnap_id == '':
|
|
70
|
+
snapctl_error(
|
|
71
|
+
message="Missing Input --byosnap-id=$your_byosnap_id",
|
|
72
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
73
|
+
if self.http_port is None and self.grpc_port is None:
|
|
74
|
+
snapctl_error(
|
|
75
|
+
message="Missing Input. One of --http-port=$your_local_server_port or --grpc-port=$your_local_server_port is required.",
|
|
76
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Static methods
|
|
80
|
+
@staticmethod
|
|
81
|
+
def stream_output(pipe):
|
|
82
|
+
'''
|
|
83
|
+
Stream the output of a subprocess to the console.
|
|
84
|
+
This function reads lines from the pipe and prints them to the console.
|
|
85
|
+
'''
|
|
86
|
+
for line in iter(pipe.readline, b''):
|
|
87
|
+
info(line.decode().rstrip())
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def _get_export_commands(snap_ids, port):
|
|
91
|
+
'''
|
|
92
|
+
Generate export commands for the given snap IDs and port.
|
|
93
|
+
'''
|
|
94
|
+
env_vars = []
|
|
95
|
+
|
|
96
|
+
for snap_id in snap_ids:
|
|
97
|
+
upper_id = snap_id.upper()
|
|
98
|
+
env_vars.append(f"SNAPEND_{upper_id}_GRPC_URL=localhost:{port}")
|
|
99
|
+
env_vars.append(
|
|
100
|
+
f"SNAPEND_{upper_id}_HTTP_URL=http://localhost:{port}")
|
|
101
|
+
|
|
102
|
+
system = platform.system()
|
|
103
|
+
|
|
104
|
+
if system == "Windows":
|
|
105
|
+
# PowerShell syntax
|
|
106
|
+
return "; ".join([f'$env:{env_vars[i].replace("=", " = ")}' for i in range(len(env_vars))])
|
|
107
|
+
else:
|
|
108
|
+
# Linux or macOS bash/zsh syntax
|
|
109
|
+
return "\n".join([f"{env_vars[i]}" for i in range(len(env_vars))])
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def _generate_env_file(snap_ids, port):
|
|
113
|
+
'''
|
|
114
|
+
Generate an environment file with the given snap IDs and port.
|
|
115
|
+
'''
|
|
116
|
+
env_lines = []
|
|
117
|
+
|
|
118
|
+
for snap_id in snap_ids:
|
|
119
|
+
upper_id = snap_id.upper()
|
|
120
|
+
env_lines.append(
|
|
121
|
+
f"export SNAPEND_{upper_id}_GRPC_URL=localhost:{port}")
|
|
122
|
+
env_lines.append(
|
|
123
|
+
f"export SNAPEND_{upper_id}_HTTP_URL=http://localhost:{port}")
|
|
124
|
+
|
|
125
|
+
system = platform.system()
|
|
126
|
+
filename = f"{Byows.BYOWS_ENV_FILE}.ps1" if system == "Windows" else \
|
|
127
|
+
f"{Byows.BYOWS_ENV_FILE}.sh"
|
|
128
|
+
env_file = get_dot_snapser_dir() / filename
|
|
129
|
+
|
|
130
|
+
with env_file.open("w") as f:
|
|
131
|
+
if system == "Windows":
|
|
132
|
+
for line in env_lines:
|
|
133
|
+
var, value = line.replace("export ", "").split("=", 1)
|
|
134
|
+
f.write(f'$env:{var} = "{value}"\n')
|
|
135
|
+
else:
|
|
136
|
+
for line in env_lines:
|
|
137
|
+
f.write(f"{line}\n")
|
|
138
|
+
|
|
139
|
+
return env_file
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def _format_portal_http_error(msg, http_err, response):
|
|
143
|
+
"""
|
|
144
|
+
Format a portal HTTP error response for display.
|
|
145
|
+
Args:
|
|
146
|
+
http_err: The HTTPError exception.
|
|
147
|
+
response: The HTTP response object.
|
|
148
|
+
Returns:
|
|
149
|
+
str: A nicely formatted error message.
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
# Check if the response content is JSON-like
|
|
153
|
+
# FIXME: The portal should always return application/json for errors, but doesn't.
|
|
154
|
+
if response.text.strip().startswith('{'):
|
|
155
|
+
error_data = response.json()
|
|
156
|
+
api_error_code = error_data.get(
|
|
157
|
+
"api_error_code", "Unknown Code")
|
|
158
|
+
message = error_data.get("message", "No message provided")
|
|
159
|
+
details = error_data.get("details", [])
|
|
160
|
+
if details:
|
|
161
|
+
details_str = "\n - " + "\n - ".join(details)
|
|
162
|
+
else:
|
|
163
|
+
details_str = ""
|
|
164
|
+
return (
|
|
165
|
+
f"Message: {msg}\n"
|
|
166
|
+
f"Exception: {http_err}\n"
|
|
167
|
+
f"Snapser Error Code: {api_error_code}\n"
|
|
168
|
+
f"Error: {message}{details_str}"
|
|
169
|
+
)
|
|
170
|
+
except Exception:
|
|
171
|
+
pass # Fallback to default error formatting if parsing fails
|
|
172
|
+
|
|
173
|
+
# Default error message if not JSON or parsing fails
|
|
174
|
+
return f"HTTP Error {http_err.response.status_code}: {http_err.response.reason}\nResponse: {response.text.strip()}"
|
|
175
|
+
|
|
176
|
+
# Private methods
|
|
177
|
+
def _setup_port_forward(
|
|
178
|
+
self, private_key, public_key, snapend_id, snap_id, port, reverse_port, reverse_grpc_port,
|
|
179
|
+
incoming_http, incoming_grpc, outgoing_http, snap_ids, ssh_connect_addr) -> bool:
|
|
180
|
+
'''
|
|
181
|
+
Setup the SSH port forward
|
|
182
|
+
'''
|
|
183
|
+
dot_snapser_dir = get_dot_snapser_dir()
|
|
184
|
+
id_file = dot_snapser_dir / Byows.SSH_FILE
|
|
185
|
+
pub_file = dot_snapser_dir / f'{Byows.SSH_FILE}.pub'
|
|
186
|
+
|
|
187
|
+
# Write public and private key files
|
|
188
|
+
with open(id_file, 'w') as f:
|
|
189
|
+
f.write(private_key)
|
|
190
|
+
if os.name != 'nt':
|
|
191
|
+
# Only chmod on Unix-like systems
|
|
192
|
+
os.chmod(id_file, 0o600)
|
|
193
|
+
|
|
194
|
+
# write the public key
|
|
195
|
+
with open(pub_file, 'w') as f:
|
|
196
|
+
f.write(public_key)
|
|
197
|
+
|
|
198
|
+
# Combine all the information into a single info() call
|
|
199
|
+
info(
|
|
200
|
+
f"Forwarding {self.base_snapend_url}/{snapend_id}/v1/{snap_id} -> http://localhost:{incoming_http}/*\n"
|
|
201
|
+
f"Your BYOSnap HTTP server should listen on: localhost:{incoming_http}\n"
|
|
202
|
+
f"Connect to other snaps over HTTP on: localhost:{outgoing_http}\n"
|
|
203
|
+
f"Set the environment variables before starting your local server:\n\n"
|
|
204
|
+
f"{Byows._get_export_commands(snap_ids, port)}\n\n"
|
|
205
|
+
f"Run the following command to set them in your session:\n\n"
|
|
206
|
+
f" source {Byows._generate_env_file(snap_ids, port)}\n\n"
|
|
207
|
+
f"Press <ctrl-c> to stop forwarding"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
ssh_addr = ssh_connect_addr
|
|
211
|
+
|
|
212
|
+
# Extract the port from the ssh_addr if present, otherwise default to 22
|
|
213
|
+
if ':' in ssh_addr:
|
|
214
|
+
ssh_host, ssh_port = ssh_addr.split(':')
|
|
215
|
+
ssh_port = int(ssh_port)
|
|
216
|
+
else:
|
|
217
|
+
ssh_host = ssh_addr
|
|
218
|
+
ssh_port = 22
|
|
219
|
+
|
|
220
|
+
ssh_command = [
|
|
221
|
+
'ssh',
|
|
222
|
+
'-q',
|
|
223
|
+
'-4', # use IPv4
|
|
224
|
+
'-o', 'ServerAliveInterval=60',
|
|
225
|
+
'-o', 'StrictHostKeyChecking=no',
|
|
226
|
+
'-o', 'UserKnownHostsFile=/dev/null',
|
|
227
|
+
'-p', str(ssh_port),
|
|
228
|
+
'-i', str(id_file),
|
|
229
|
+
'-N',
|
|
230
|
+
'-L', f'{outgoing_http}:localhost:{port}',
|
|
231
|
+
'-R', f'{reverse_port}:localhost:{incoming_http}',
|
|
232
|
+
]
|
|
233
|
+
if incoming_grpc:
|
|
234
|
+
ssh_command += ['-R', f'{reverse_grpc_port}:localhost:{incoming_grpc}']
|
|
235
|
+
ssh_command += [ssh_host]
|
|
236
|
+
|
|
237
|
+
process = None
|
|
238
|
+
try:
|
|
239
|
+
process = subprocess.Popen(
|
|
240
|
+
ssh_command,
|
|
241
|
+
shell=False,
|
|
242
|
+
stdout=subprocess.PIPE,
|
|
243
|
+
stderr=subprocess.PIPE
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Give it a few seconds to report any immediate errors
|
|
247
|
+
timeout_seconds = 5
|
|
248
|
+
start_time = time.time()
|
|
249
|
+
while True:
|
|
250
|
+
if process.poll() is not None:
|
|
251
|
+
# Process exited early — likely an error
|
|
252
|
+
stderr_output = process.stderr.read().decode().strip()
|
|
253
|
+
if stderr_output:
|
|
254
|
+
info(f"[SSH Error] {stderr_output}")
|
|
255
|
+
else:
|
|
256
|
+
warning("SSH process exited unexpectedly with no output.")
|
|
257
|
+
return False
|
|
258
|
+
|
|
259
|
+
if time.time() - start_time > timeout_seconds:
|
|
260
|
+
break
|
|
261
|
+
|
|
262
|
+
time.sleep(0.2)
|
|
263
|
+
|
|
264
|
+
# Start background thread to stream live stderr
|
|
265
|
+
threading.Thread(target=Byows.stream_output, args=(
|
|
266
|
+
process.stderr,), daemon=True).start()
|
|
267
|
+
|
|
268
|
+
# Now block for the full tunnel lifetime
|
|
269
|
+
process.wait(timeout=Byows.PORT_FORWARD_TTL)
|
|
270
|
+
except KeyboardInterrupt:
|
|
271
|
+
process.terminate()
|
|
272
|
+
try:
|
|
273
|
+
process.wait(timeout=5)
|
|
274
|
+
except subprocess.TimeoutExpired:
|
|
275
|
+
warning('SSH process did not shut down gracefully.')
|
|
276
|
+
|
|
277
|
+
info('Shutting down port forward...')
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
url = f"{self.base_url}/v1/snapser-api/byows/snapends/" + \
|
|
281
|
+
f"{self.snapend_id}/snaps/{self.byosnap_id}/enabled"
|
|
282
|
+
res = requests.put(
|
|
283
|
+
url, headers={'api-key': self.api_key}, json=False, timeout=SERVER_CALL_TIMEOUT)
|
|
284
|
+
res.raise_for_status()
|
|
285
|
+
info('Forwarding disabled')
|
|
286
|
+
except HTTPError as http_err:
|
|
287
|
+
snapctl_error(
|
|
288
|
+
message=Byows._format_portal_http_error(
|
|
289
|
+
"Unable to disable BYOWs", http_err, res),
|
|
290
|
+
code=SNAPCTL_BYOWS_GENERIC_ERROR
|
|
291
|
+
)
|
|
292
|
+
return False
|
|
293
|
+
except RequestException as req_err:
|
|
294
|
+
warning(
|
|
295
|
+
f"Response Status Code: {res.status_code}, Body: {res.text}")
|
|
296
|
+
snapctl_error(
|
|
297
|
+
message=f"Request Exception: Unable to disable BYOWs {req_err}",
|
|
298
|
+
code=SNAPCTL_BYOWS_GENERIC_ERROR
|
|
299
|
+
)
|
|
300
|
+
return False
|
|
301
|
+
except Exception as e:
|
|
302
|
+
snapctl_error(
|
|
303
|
+
message=f"Unexpected error occurred: {e}",
|
|
304
|
+
code=SNAPCTL_BYOWS_GENERIC_ERROR
|
|
305
|
+
)
|
|
306
|
+
return False
|
|
307
|
+
|
|
308
|
+
except Exception as e:
|
|
309
|
+
print('Error running SSH command:', e)
|
|
310
|
+
return False
|
|
311
|
+
finally:
|
|
312
|
+
# Cleanup the SSH key files
|
|
313
|
+
# Only remove the files if they exist
|
|
314
|
+
try:
|
|
315
|
+
id_file.unlink()
|
|
316
|
+
pub_file.unlink()
|
|
317
|
+
except Exception as e:
|
|
318
|
+
warning(
|
|
319
|
+
f"Cleanup warning: Failed to delete SSH key files – {e}")
|
|
320
|
+
return True
|
|
321
|
+
|
|
322
|
+
# Commands
|
|
323
|
+
def attach(self) -> bool:
|
|
324
|
+
"""
|
|
325
|
+
BYOWs port forward
|
|
326
|
+
"""
|
|
327
|
+
progress = Progress(
|
|
328
|
+
SpinnerColumn(),
|
|
329
|
+
TextColumn("[progress.description]{task.description}"),
|
|
330
|
+
transient=True,
|
|
331
|
+
)
|
|
332
|
+
progress.start()
|
|
333
|
+
progress.add_task(description='Setting up BYOWs port forward...', total=None)
|
|
334
|
+
try:
|
|
335
|
+
url = f"{self.base_url}/v1/snapser-api/byows/snapends/" + \
|
|
336
|
+
f"{self.snapend_id}/snaps/{self.byosnap_id}"
|
|
337
|
+
res = requests.put(
|
|
338
|
+
url,
|
|
339
|
+
headers={'api-key': self.api_key},
|
|
340
|
+
json={
|
|
341
|
+
"snapend_id": self.snapend_id,
|
|
342
|
+
"snap_id": self.byosnap_id
|
|
343
|
+
},
|
|
344
|
+
timeout=SERVER_CALL_TIMEOUT
|
|
345
|
+
)
|
|
346
|
+
res.raise_for_status()
|
|
347
|
+
response_json = res.json()
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
if res.ok and 'workstationPort' in response_json and \
|
|
351
|
+
'workstationReversePort' in response_json and \
|
|
352
|
+
'snapendId' in response_json and \
|
|
353
|
+
'proxyPrivateKey' in response_json and \
|
|
354
|
+
'proxyPublicKey' in response_json:
|
|
355
|
+
|
|
356
|
+
# We do not use the workstationReversePort now, we ask the user
|
|
357
|
+
# to provide the port they want to use
|
|
358
|
+
# incoming_http_port = response_json['workstationReversePort']
|
|
359
|
+
incoming_http_port = self.http_port
|
|
360
|
+
incoming_grpc_port = self.grpc_port
|
|
361
|
+
|
|
362
|
+
outgoing_http_port = response_json['workstationPort']
|
|
363
|
+
|
|
364
|
+
progress.stop()
|
|
365
|
+
|
|
366
|
+
self._setup_port_forward(
|
|
367
|
+
response_json['proxyPrivateKey'],
|
|
368
|
+
response_json['proxyPublicKey'],
|
|
369
|
+
response_json['snapendId'],
|
|
370
|
+
response_json['snapId'],
|
|
371
|
+
response_json['workstationPort'],
|
|
372
|
+
response_json['workstationReversePort'],
|
|
373
|
+
response_json['workstationReverseGrpcPort'],
|
|
374
|
+
incoming_http_port,
|
|
375
|
+
incoming_grpc_port,
|
|
376
|
+
outgoing_http_port,
|
|
377
|
+
response_json['snapIds'],
|
|
378
|
+
response_json['sshConnectAddr'],
|
|
379
|
+
)
|
|
380
|
+
return snapctl_success(
|
|
381
|
+
message='complete', progress=progress)
|
|
382
|
+
snapctl_error(
|
|
383
|
+
message='Unable to setup port forward.',
|
|
384
|
+
code=SNAPCTL_BYOWS_GENERIC_ERROR, progress=progress)
|
|
385
|
+
except HTTPError as http_err:
|
|
386
|
+
snapctl_error(
|
|
387
|
+
message=Byows._format_portal_http_error(
|
|
388
|
+
"Unable to setup port forward", http_err, res),
|
|
389
|
+
code=SNAPCTL_BYOWS_GENERIC_ERROR, progress=progress)
|
|
390
|
+
snapctl_error(
|
|
391
|
+
message=f"Server Error: Unable to setup port forward {http_err}",
|
|
392
|
+
code=SNAPCTL_BYOWS_GENERIC_ERROR, progress=progress)
|
|
393
|
+
except RequestException as e:
|
|
394
|
+
snapctl_error(
|
|
395
|
+
message=f"Exception: Unable to setup port forward {e}",
|
|
396
|
+
code=SNAPCTL_BYOWS_GENERIC_ERROR, progress=progress)
|
|
397
|
+
finally:
|
|
398
|
+
progress.stop()
|
|
399
|
+
snapctl_error(
|
|
400
|
+
message='Failed to setup port forward.',
|
|
401
|
+
code=SNAPCTL_BYOWS_GENERIC_ERROR, progress=progress)
|
snapctl/commands/game.py
CHANGED
|
@@ -40,7 +40,7 @@ class Game:
|
|
|
40
40
|
# Check subcommand
|
|
41
41
|
if not self.subcommand in Game.SUBCOMMANDS:
|
|
42
42
|
snapctl_error(
|
|
43
|
-
message="Invalid command. Valid commands are" +
|
|
43
|
+
message="Invalid command. Valid commands are " +
|
|
44
44
|
f"{', '.join(Game.SUBCOMMANDS)}.",
|
|
45
45
|
code=SNAPCTL_INPUT_ERROR)
|
|
46
46
|
# Check sdk-download commands
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Release Notes
|
|
2
|
+
Release Notes
|
|
3
3
|
"""
|
|
4
|
-
import os
|
|
5
4
|
from typing import Union
|
|
5
|
+
import importlib.resources as pkg_resources
|
|
6
|
+
import snapctl.data.releases # must have __init__.py under releases
|
|
6
7
|
from snapctl.config.constants import SNAPCTL_INPUT_ERROR
|
|
7
8
|
from snapctl.utils.helper import snapctl_error, snapctl_success
|
|
8
9
|
|
|
@@ -12,7 +13,6 @@ class ReleaseNotes:
|
|
|
12
13
|
Release Notes Command
|
|
13
14
|
"""
|
|
14
15
|
SUBCOMMANDS = ["releases", "show"]
|
|
15
|
-
RELEASES_PATH = 'snapctl/data/releases'
|
|
16
16
|
|
|
17
17
|
def __init__(self, *, subcommand: str, version: Union[str, None] = None) -> None:
|
|
18
18
|
self.subcommand = subcommand
|
|
@@ -29,19 +29,15 @@ class ReleaseNotes:
|
|
|
29
29
|
f"{', '.join(ReleaseNotes.SUBCOMMANDS)}.",
|
|
30
30
|
code=SNAPCTL_INPUT_ERROR)
|
|
31
31
|
|
|
32
|
-
# Upper echelon commands
|
|
33
32
|
def releases(self) -> None:
|
|
34
33
|
"""
|
|
35
34
|
List versions
|
|
36
35
|
"""
|
|
37
|
-
# List all files and directories in the specified path
|
|
38
|
-
files_and_directories = os.listdir(ReleaseNotes.RELEASES_PATH)
|
|
39
|
-
|
|
40
|
-
# Print only files, excluding subdirectories
|
|
41
36
|
print('== Releases ' + '=' * (92))
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
# List all resource files in snapctl.data.releases
|
|
38
|
+
for resource in pkg_resources.contents(snapctl.data.releases):
|
|
39
|
+
if resource.endswith('.mdx'):
|
|
40
|
+
print(resource.replace('.mdx', '').replace('.md', ''))
|
|
45
41
|
print('=' * (104))
|
|
46
42
|
snapctl_success(message="List versions")
|
|
47
43
|
|
|
@@ -49,17 +45,15 @@ class ReleaseNotes:
|
|
|
49
45
|
"""
|
|
50
46
|
Show version
|
|
51
47
|
"""
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if not os.path.isfile(version_file):
|
|
48
|
+
version_filename = f"{self.version}.mdx"
|
|
49
|
+
|
|
50
|
+
if version_filename not in pkg_resources.contents(snapctl.data.releases):
|
|
56
51
|
snapctl_error(
|
|
57
52
|
message=f"Version {self.version} does not exist.",
|
|
58
53
|
code=SNAPCTL_INPUT_ERROR)
|
|
59
54
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
with open(version_file, 'r') as file:
|
|
55
|
+
print('== Release Notes ' + '=' * (86))
|
|
56
|
+
with pkg_resources.open_text(snapctl.data.releases, version_filename) as file:
|
|
63
57
|
print(file.read())
|
|
64
58
|
print('=' * (104))
|
|
65
59
|
snapctl_success(message=f"Show version {self.version}")
|
snapctl/commands/snapend.py
CHANGED
|
@@ -37,7 +37,9 @@ class Snapend:
|
|
|
37
37
|
DOWNLOAD_CATEGORY = [
|
|
38
38
|
'sdk', 'protos', 'snapend-manifest'
|
|
39
39
|
]
|
|
40
|
-
CATEGORY_TYPE_SDK = [
|
|
40
|
+
CATEGORY_TYPE_SDK = [
|
|
41
|
+
# 'omni',
|
|
42
|
+
'user', 'api-key', 'internal', 'app']
|
|
41
43
|
CATEGORY_TYPE_PROTOS = ['messages', 'services']
|
|
42
44
|
# CATEGORY_TYPE_HTTP_LIB_FORMATS = ['unity', 'web-ts', 'ts']
|
|
43
45
|
|
|
@@ -412,26 +414,26 @@ class Snapend:
|
|
|
412
414
|
)
|
|
413
415
|
# Special cases for client SDKs
|
|
414
416
|
if self.category_format in CLIENT_SDK_TYPES:
|
|
415
|
-
if self.category_type in ['
|
|
417
|
+
if self.category_type in ['api-key', 'app', 'internal']:
|
|
416
418
|
snapctl_error(
|
|
417
419
|
message="Invalid combination of format and type. " +
|
|
418
420
|
", ".join(CLIENT_SDK_TYPES.keys()) +
|
|
421
|
+
# " SDKs are only available for --type=omni or --type=user",
|
|
419
422
|
" SDKs are only available for --type=user",
|
|
420
423
|
code=SNAPCTL_INPUT_ERROR
|
|
421
424
|
)
|
|
422
|
-
if self.category_type == 'internal':
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
425
|
+
# if self.category_type == 'internal':
|
|
426
|
+
# snapctl_error(
|
|
427
|
+
# message="Internal access type is not supported for " +
|
|
428
|
+
# ", ".join(CLIENT_SDK_TYPES.keys()) + " SDKs.",
|
|
429
|
+
# code=SNAPCTL_INPUT_ERROR
|
|
430
|
+
# )
|
|
428
431
|
if self.category_http_lib:
|
|
429
432
|
# First check if the format supports http-lib
|
|
430
433
|
if self.category_format in Snapend.get_formats_supporting_http_lib():
|
|
431
434
|
# Check if the http-lib is supported for the format
|
|
432
435
|
valid_http_libs = SDK_TYPES[self.category_format]['http-lib']
|
|
433
436
|
if self.category_http_lib not in valid_http_libs:
|
|
434
|
-
print('Caught you')
|
|
435
437
|
snapctl_error(
|
|
436
438
|
message="Invalid HTTP Library. Valid libraries are " +
|
|
437
439
|
Snapend.get_http_formats_str(),
|
snapctl/config/constants.py
CHANGED
|
@@ -3,7 +3,7 @@ Constants used by snapctl
|
|
|
3
3
|
"""
|
|
4
4
|
COMPANY_NAME = 'Snapser'
|
|
5
5
|
VERSION_PREFIX = 'beta-'
|
|
6
|
-
VERSION = '0.
|
|
6
|
+
VERSION = '0.49.1'
|
|
7
7
|
CONFIG_FILE_MAC = '~/.snapser/config'
|
|
8
8
|
CONFIG_FILE_WIN = '%homepath%\\.snapser\\config'
|
|
9
9
|
|
|
@@ -50,7 +50,7 @@ SNAPCTL_BYOGS_PUBLISH_ERROR = 25
|
|
|
50
50
|
SNAPCTL_BYOGS_PUBLISH_PERMISSION_ERROR = 26
|
|
51
51
|
SNAPCTL_BYOGS_PUBLISH_DUPLICATE_TAG_ERROR = 27
|
|
52
52
|
|
|
53
|
-
# BYOSNAP Errors - 30 - 49 then 86 -
|
|
53
|
+
# BYOSNAP Errors - 30 - 49 then 86 - 90
|
|
54
54
|
SNAPCTL_BYOSNAP_NOT_FOUND = SNAPCTL_RESOURCE_NOT_FOUND
|
|
55
55
|
SNAPCTL_BYOSNAP_GENERIC_ERROR = 30
|
|
56
56
|
SNAPCTL_BYOSNAP_DEPENDENCY_MISSING = 31
|
|
@@ -108,3 +108,6 @@ SNAPCTL_SNAPEND_APPLY_MANIFEST_MISMATCH_ERROR = 76
|
|
|
108
108
|
# Generate Errors - 80 - 85
|
|
109
109
|
SNAPCTL_GENERATE_GENERIC_ERROR = 80
|
|
110
110
|
SNAPCTL_GENERATE_CREDENTIALS_ERROR = 81
|
|
111
|
+
|
|
112
|
+
# BYOWs Errors - 95 - 99
|
|
113
|
+
SNAPCTL_BYOWS_GENERIC_ERROR = 95
|
snapctl/config/endpoints.py
CHANGED
|
@@ -9,3 +9,8 @@ END_POINTS: Dict[str, str] = {
|
|
|
9
9
|
'PLAYTEST': 'https://gateway.dev.snapser.io/playtest',
|
|
10
10
|
'PROD': 'https://gateway.snapser.com/snapser'
|
|
11
11
|
}
|
|
12
|
+
|
|
13
|
+
GATEWAY_END_POINTS: Dict[str, str] = {
|
|
14
|
+
'SANDBOX': 'https://gateway.dev.snapser.io',
|
|
15
|
+
'LIVE': 'https://gateway-accel.snapser.com',
|
|
16
|
+
}
|
snapctl/config/hashes.py
CHANGED
|
@@ -189,11 +189,15 @@ SERVICE_IDS: List[str] = [
|
|
|
189
189
|
]
|
|
190
190
|
|
|
191
191
|
SDK_ACCESS_AUTH_TYPE_LOOKUP: Dict[str, Dict[str, str]] = {
|
|
192
|
+
# 'omni': {
|
|
193
|
+
# 'access_type': 'external',
|
|
194
|
+
# 'auth_type': 'omni'
|
|
195
|
+
# },
|
|
192
196
|
'user': {
|
|
193
197
|
'access_type': 'external',
|
|
194
198
|
'auth_type': 'user'
|
|
195
199
|
},
|
|
196
|
-
'
|
|
200
|
+
'api-key': {
|
|
197
201
|
'access_type': 'external',
|
|
198
202
|
'auth_type': 'api-key'
|
|
199
203
|
},
|
snapctl/data/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## beta-0.49.0
|
|
2
|
+
##### May 5, 2025
|
|
3
|
+
|
|
4
|
+
### Breaking Change
|
|
5
|
+
1. Renamed SDK type `server` to `api-key` to be consistent with the Snapser Web app.
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
1. The `snapctl byosnap generate-profile` command was not working when outside the root snapctl folder. This is now fixed.
|
|
9
|
+
2. The `snapctl release-notes` commands were not working when outside the root snapctl folder. This is now fixed.
|