snapctl 0.48.0__py3-none-any.whl → 0.49.0__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 +421 -0
- snapctl/commands/game.py +1 -1
- snapctl/commands/release_notes.py +12 -18
- snapctl/commands/snapend.py +26 -11
- snapctl/config/constants.py +7 -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 +12 -0
- snapctl/main.py +72 -1
- snapctl/utils/helper.py +9 -0
- {snapctl-0.48.0.dist-info → snapctl-0.49.0.dist-info}/METADATA +159 -97
- {snapctl-0.48.0.dist-info → snapctl-0.49.0.dist-info}/RECORD +19 -14
- {snapctl-0.48.0.dist-info → snapctl-0.49.0.dist-info}/LICENSE +0 -0
- {snapctl-0.48.0.dist-info → snapctl-0.49.0.dist-info}/WHEEL +0 -0
- {snapctl-0.48.0.dist-info → snapctl-0.49.0.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,421 @@
|
|
|
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 _write_export_command_to_console(snap_ids, port):
|
|
91
|
+
'''
|
|
92
|
+
TODO: This is not used anymore
|
|
93
|
+
Generate export commands for the given snap IDs and port.
|
|
94
|
+
'''
|
|
95
|
+
env_vars = []
|
|
96
|
+
|
|
97
|
+
for snap_id in snap_ids:
|
|
98
|
+
upper_id = snap_id.upper()
|
|
99
|
+
env_vars.append(f"SNAPEND_{upper_id}_GRPC_URL=localhost:{port}")
|
|
100
|
+
env_vars.append(
|
|
101
|
+
f"SNAPEND_{upper_id}_HTTP_URL=http://localhost:{port}")
|
|
102
|
+
|
|
103
|
+
system = platform.system()
|
|
104
|
+
|
|
105
|
+
if system == "Windows":
|
|
106
|
+
# PowerShell syntax
|
|
107
|
+
return "; ".join([f'$env:{env_vars[i].replace("=", " = ")}; $env:{env_vars[i+1].replace("=", " = ")}'
|
|
108
|
+
for i in range(0, len(env_vars), 2)])
|
|
109
|
+
else:
|
|
110
|
+
# Linux or macOS bash/zsh syntax
|
|
111
|
+
return "export \\\n" + " \\\n".join([f" {env_vars[i]} {env_vars[i+1]}"
|
|
112
|
+
for i in range(0, len(env_vars), 2)])
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def _generate_env_file(snap_ids, port):
|
|
116
|
+
'''
|
|
117
|
+
Generate an environment file with the given snap IDs and port.
|
|
118
|
+
'''
|
|
119
|
+
env_lines = []
|
|
120
|
+
|
|
121
|
+
for snap_id in snap_ids:
|
|
122
|
+
upper_id = snap_id.upper()
|
|
123
|
+
env_lines.append(
|
|
124
|
+
f"export SNAPEND_{upper_id}_GRPC_URL=localhost:{port}")
|
|
125
|
+
env_lines.append(
|
|
126
|
+
f"export SNAPEND_{upper_id}_HTTP_URL=http://localhost:{port}")
|
|
127
|
+
|
|
128
|
+
system = platform.system()
|
|
129
|
+
filename = f"{Byows.BYOWS_ENV_FILE}.ps1" if system == "Windows" else \
|
|
130
|
+
f"{Byows.BYOWS_ENV_FILE}.sh"
|
|
131
|
+
env_file = get_dot_snapser_dir() / filename
|
|
132
|
+
|
|
133
|
+
with env_file.open("w") as f:
|
|
134
|
+
if system == "Windows":
|
|
135
|
+
for line in env_lines:
|
|
136
|
+
var, value = line.replace("export ", "").split("=", 1)
|
|
137
|
+
f.write(f'$env:{var} = "{value}"\n')
|
|
138
|
+
else:
|
|
139
|
+
for line in env_lines:
|
|
140
|
+
f.write(f"{line}\n")
|
|
141
|
+
|
|
142
|
+
return env_file
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def _generate_env(snap_ids, port):
|
|
146
|
+
'''
|
|
147
|
+
Generate environment variables for the given snap IDs and port.
|
|
148
|
+
'''
|
|
149
|
+
env_file = Byows._generate_env_file(snap_ids, port)
|
|
150
|
+
info(f"Environment variables written to {env_file}")
|
|
151
|
+
system = platform.system()
|
|
152
|
+
if system == "Windows":
|
|
153
|
+
info(
|
|
154
|
+
f"Run the following command to set them in your session:\n\n . {env_file}\n")
|
|
155
|
+
else:
|
|
156
|
+
info(
|
|
157
|
+
f"Run the following command to set them in your shell:\n\n source {env_file}\n")
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def _format_portal_http_error(msg, http_err, response):
|
|
161
|
+
"""
|
|
162
|
+
Format a portal HTTP error response for display.
|
|
163
|
+
Args:
|
|
164
|
+
http_err: The HTTPError exception.
|
|
165
|
+
response: The HTTP response object.
|
|
166
|
+
Returns:
|
|
167
|
+
str: A nicely formatted error message.
|
|
168
|
+
"""
|
|
169
|
+
try:
|
|
170
|
+
# Check if the response content is JSON-like
|
|
171
|
+
# FIXME: The portal should always return application/json for errors, but doesn't.
|
|
172
|
+
if response.text.strip().startswith('{'):
|
|
173
|
+
error_data = response.json()
|
|
174
|
+
api_error_code = error_data.get(
|
|
175
|
+
"api_error_code", "Unknown Code")
|
|
176
|
+
message = error_data.get("message", "No message provided")
|
|
177
|
+
details = error_data.get("details", [])
|
|
178
|
+
if details:
|
|
179
|
+
details_str = "\n - " + "\n - ".join(details)
|
|
180
|
+
else:
|
|
181
|
+
details_str = ""
|
|
182
|
+
return (
|
|
183
|
+
f"Message: {msg}\n"
|
|
184
|
+
f"Exception: {http_err}\n"
|
|
185
|
+
f"Snapser Error Code: {api_error_code}\n"
|
|
186
|
+
f"Error: {message}{details_str}"
|
|
187
|
+
)
|
|
188
|
+
except Exception:
|
|
189
|
+
pass # Fallback to default error formatting if parsing fails
|
|
190
|
+
|
|
191
|
+
# Default error message if not JSON or parsing fails
|
|
192
|
+
return f"HTTP Error {http_err.response.status_code}: {http_err.response.reason}\nResponse: {response.text.strip()}"
|
|
193
|
+
|
|
194
|
+
# Private methods
|
|
195
|
+
def _setup_port_forward(
|
|
196
|
+
self, private_key, public_key, snapend_id, snap_id, port, reverse_port, reverse_grpc_port,
|
|
197
|
+
incoming_http, incoming_grpc, outgoing_http, snap_ids, ssh_connect_addr) -> bool:
|
|
198
|
+
'''
|
|
199
|
+
Setup the SSH port forward
|
|
200
|
+
'''
|
|
201
|
+
dot_snapser_dir = get_dot_snapser_dir()
|
|
202
|
+
id_file = dot_snapser_dir / Byows.SSH_FILE
|
|
203
|
+
pub_file = dot_snapser_dir / f'{Byows.SSH_FILE}.pub'
|
|
204
|
+
|
|
205
|
+
# Write public and private key files
|
|
206
|
+
with open(id_file, 'w') as f:
|
|
207
|
+
f.write(private_key)
|
|
208
|
+
if os.name != 'nt':
|
|
209
|
+
# Only chmod on Unix-like systems
|
|
210
|
+
os.chmod(id_file, 0o600)
|
|
211
|
+
|
|
212
|
+
# write the public key
|
|
213
|
+
with open(pub_file, 'w') as f:
|
|
214
|
+
f.write(public_key)
|
|
215
|
+
|
|
216
|
+
# Let user know about the forwarding
|
|
217
|
+
# TODO: AJ, needs to be the proper base url
|
|
218
|
+
info(f"Forwarding {self.base_snapend_url}/{snapend_id}/v1/{snap_id}" +
|
|
219
|
+
f"-> http://localhost:{incoming_http}/*")
|
|
220
|
+
info(
|
|
221
|
+
f"Your BYOSnap HTTP server should listen on: localhost:{incoming_http}")
|
|
222
|
+
info(
|
|
223
|
+
f"Connect to other snaps over HTTP on: localhost:{outgoing_http}")
|
|
224
|
+
Byows._generate_env(snap_ids, port)
|
|
225
|
+
info('Press <ctrl-c> to stop forwarding')
|
|
226
|
+
|
|
227
|
+
# TODO: ??? AJ get the ssh host for the current environ from BYOWS_SSH_HOSTS contact in endpoints.py
|
|
228
|
+
ssh_addr = ssh_connect_addr
|
|
229
|
+
if not ssh_addr:
|
|
230
|
+
snapctl_error(message=f"{env} is missing or empty in BYOWS_SSH_HOSTS", code=SNAPCTL_INPUT_ERROR)
|
|
231
|
+
|
|
232
|
+
# Extract the port from the ssh_addr if present, otherwise default to 22
|
|
233
|
+
if ':' in ssh_addr:
|
|
234
|
+
ssh_host, ssh_port = ssh_addr.split(':')
|
|
235
|
+
ssh_port = int(ssh_port)
|
|
236
|
+
else:
|
|
237
|
+
ssh_host = ssh_addr
|
|
238
|
+
ssh_port = 22
|
|
239
|
+
|
|
240
|
+
ssh_command = [
|
|
241
|
+
'ssh',
|
|
242
|
+
'-o', 'ServerAliveInterval=60',
|
|
243
|
+
'-o', 'StrictHostKeyChecking=no',
|
|
244
|
+
'-o', 'UserKnownHostsFile=/dev/null',
|
|
245
|
+
'-p', str(ssh_port),
|
|
246
|
+
'-i', str(id_file),
|
|
247
|
+
'-N',
|
|
248
|
+
'-L', f'{outgoing_http}:localhost:{port}',
|
|
249
|
+
'-R', f'{reverse_port}:localhost:{incoming_http}',
|
|
250
|
+
]
|
|
251
|
+
if incoming_grpc:
|
|
252
|
+
ssh_command += ['-R', f'{reverse_grpc_port}:localhost:{incoming_grpc}']
|
|
253
|
+
ssh_command += [ssh_host]
|
|
254
|
+
|
|
255
|
+
# print(f'Running SSH command: {ssh_command}')
|
|
256
|
+
process = None
|
|
257
|
+
try:
|
|
258
|
+
process = subprocess.Popen(
|
|
259
|
+
ssh_command,
|
|
260
|
+
shell=False,
|
|
261
|
+
stdout=subprocess.PIPE,
|
|
262
|
+
stderr=subprocess.PIPE
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Give it a few seconds to report any immediate errors
|
|
266
|
+
timeout_seconds = 5
|
|
267
|
+
start_time = time.time()
|
|
268
|
+
while True:
|
|
269
|
+
if process.poll() is not None:
|
|
270
|
+
# Process exited early — likely an error
|
|
271
|
+
stderr_output = process.stderr.read().decode().strip()
|
|
272
|
+
if stderr_output:
|
|
273
|
+
info(f"[SSH Error] {stderr_output}")
|
|
274
|
+
else:
|
|
275
|
+
warning("SSH process exited unexpectedly with no output.")
|
|
276
|
+
return False
|
|
277
|
+
|
|
278
|
+
if time.time() - start_time > timeout_seconds:
|
|
279
|
+
break
|
|
280
|
+
|
|
281
|
+
time.sleep(0.2)
|
|
282
|
+
|
|
283
|
+
# Start background thread to stream live stderr
|
|
284
|
+
threading.Thread(target=Byows.stream_output, args=(
|
|
285
|
+
process.stderr,), daemon=True).start()
|
|
286
|
+
|
|
287
|
+
# Now block for the full tunnel lifetime
|
|
288
|
+
process.wait(timeout=Byows.PORT_FORWARD_TTL)
|
|
289
|
+
except KeyboardInterrupt:
|
|
290
|
+
process.terminate()
|
|
291
|
+
try:
|
|
292
|
+
process.wait(timeout=5)
|
|
293
|
+
except subprocess.TimeoutExpired:
|
|
294
|
+
warning('SSH process did not shut down gracefully.')
|
|
295
|
+
|
|
296
|
+
info('Shutting down port forward...')
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
url = f"{self.base_url}/v1/snapser-api/byows/snapends/" + \
|
|
300
|
+
f"{self.snapend_id}/snaps/{self.byosnap_id}/enabled"
|
|
301
|
+
res = requests.put(
|
|
302
|
+
url, headers={'api-key': self.api_key}, json=False, timeout=SERVER_CALL_TIMEOUT)
|
|
303
|
+
res.raise_for_status()
|
|
304
|
+
info('Forwarding disabled')
|
|
305
|
+
except HTTPError as http_err:
|
|
306
|
+
snapctl_error(
|
|
307
|
+
message=Byows._format_portal_http_error(
|
|
308
|
+
"Unable to disable BYOWs", http_err, res),
|
|
309
|
+
code=SNAPCTL_BYOWS_GENERIC_ERROR
|
|
310
|
+
)
|
|
311
|
+
return False
|
|
312
|
+
except RequestException as req_err:
|
|
313
|
+
warning(
|
|
314
|
+
f"Response Status Code: {res.status_code}, Body: {res.text}")
|
|
315
|
+
snapctl_error(
|
|
316
|
+
message=f"Request Exception: Unable to disable BYOWs {req_err}",
|
|
317
|
+
code=SNAPCTL_BYOWS_GENERIC_ERROR
|
|
318
|
+
)
|
|
319
|
+
return False
|
|
320
|
+
except Exception as e:
|
|
321
|
+
snapctl_error(
|
|
322
|
+
message=f"Unexpected error occurred: {e}",
|
|
323
|
+
code=SNAPCTL_BYOWS_GENERIC_ERROR
|
|
324
|
+
)
|
|
325
|
+
return False
|
|
326
|
+
|
|
327
|
+
except Exception as e:
|
|
328
|
+
print('Error running SSH command:', e)
|
|
329
|
+
return False
|
|
330
|
+
finally:
|
|
331
|
+
# Cleanup the SSH key files
|
|
332
|
+
# Only remove the files if they exist
|
|
333
|
+
try:
|
|
334
|
+
id_file.unlink()
|
|
335
|
+
pub_file.unlink()
|
|
336
|
+
except Exception as e:
|
|
337
|
+
warning(
|
|
338
|
+
f"Cleanup warning: Failed to delete SSH key files – {e}")
|
|
339
|
+
return True
|
|
340
|
+
|
|
341
|
+
# Commands
|
|
342
|
+
def attach(self) -> bool:
|
|
343
|
+
"""
|
|
344
|
+
BYOWs port forward
|
|
345
|
+
"""
|
|
346
|
+
progress = Progress(
|
|
347
|
+
SpinnerColumn(),
|
|
348
|
+
TextColumn("[progress.description]{task.description}"),
|
|
349
|
+
transient=True,
|
|
350
|
+
)
|
|
351
|
+
progress.start()
|
|
352
|
+
progress.add_task(
|
|
353
|
+
description='Setting up BYOWs port forward...', total=None)
|
|
354
|
+
try:
|
|
355
|
+
url = f"{self.base_url}/v1/snapser-api/byows/snapends/" + \
|
|
356
|
+
f"{self.snapend_id}/snaps/{self.byosnap_id}"
|
|
357
|
+
res = requests.put(
|
|
358
|
+
url,
|
|
359
|
+
headers={'api-key': self.api_key},
|
|
360
|
+
json={
|
|
361
|
+
"snapend_id": self.snapend_id,
|
|
362
|
+
"snap_id": self.byosnap_id
|
|
363
|
+
},
|
|
364
|
+
timeout=SERVER_CALL_TIMEOUT
|
|
365
|
+
)
|
|
366
|
+
res.raise_for_status()
|
|
367
|
+
response_json = res.json()
|
|
368
|
+
|
|
369
|
+
# For debugging
|
|
370
|
+
#print(f"BYOWS Response:\n{json.dumps(response_json, indent=2)}")
|
|
371
|
+
|
|
372
|
+
if res.ok and 'workstationPort' in response_json and \
|
|
373
|
+
'workstationReversePort' in response_json and \
|
|
374
|
+
'snapendId' in response_json and \
|
|
375
|
+
'proxyPrivateKey' in response_json and \
|
|
376
|
+
'proxyPublicKey' in response_json:
|
|
377
|
+
|
|
378
|
+
# We do not use the workstationReversePort now, we ask the user
|
|
379
|
+
# to provide the port they want to use
|
|
380
|
+
# incoming_http_port = response_json['workstationReversePort']
|
|
381
|
+
incoming_http_port = self.http_port
|
|
382
|
+
incoming_grpc_port = self.grpc_port
|
|
383
|
+
|
|
384
|
+
outgoing_http_port = response_json['workstationPort']
|
|
385
|
+
|
|
386
|
+
self._setup_port_forward(
|
|
387
|
+
response_json['proxyPrivateKey'],
|
|
388
|
+
response_json['proxyPublicKey'],
|
|
389
|
+
response_json['snapendId'],
|
|
390
|
+
response_json['snapId'],
|
|
391
|
+
response_json['workstationPort'],
|
|
392
|
+
response_json['workstationReversePort'],
|
|
393
|
+
response_json['workstationReverseGrpcPort'],
|
|
394
|
+
incoming_http_port,
|
|
395
|
+
incoming_grpc_port,
|
|
396
|
+
outgoing_http_port,
|
|
397
|
+
response_json['snapIds'],
|
|
398
|
+
response_json['sshConnectAddr'],
|
|
399
|
+
)
|
|
400
|
+
return snapctl_success(
|
|
401
|
+
message='complete', progress=progress)
|
|
402
|
+
snapctl_error(
|
|
403
|
+
message='Unable to setup port forward.',
|
|
404
|
+
code=SNAPCTL_BYOWS_GENERIC_ERROR, progress=progress)
|
|
405
|
+
except HTTPError as http_err:
|
|
406
|
+
snapctl_error(
|
|
407
|
+
message=Byows._format_portal_http_error(
|
|
408
|
+
"Unable to setup port forward", http_err, res),
|
|
409
|
+
code=SNAPCTL_BYOWS_GENERIC_ERROR, progress=progress)
|
|
410
|
+
snapctl_error(
|
|
411
|
+
message=f"Server Error: Unable to setup port forward {http_err}",
|
|
412
|
+
code=SNAPCTL_BYOWS_GENERIC_ERROR, progress=progress)
|
|
413
|
+
except RequestException as e:
|
|
414
|
+
snapctl_error(
|
|
415
|
+
message=f"Exception: Unable to setup port forward {e}",
|
|
416
|
+
code=SNAPCTL_BYOWS_GENERIC_ERROR, progress=progress)
|
|
417
|
+
finally:
|
|
418
|
+
progress.stop()
|
|
419
|
+
snapctl_error(
|
|
420
|
+
message='Failed to setup port forward.',
|
|
421
|
+
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
|
@@ -18,7 +18,8 @@ from snapctl.config.constants import SERVER_CALL_TIMEOUT, SNAPCTL_INPUT_ERROR, \
|
|
|
18
18
|
SNAPCTL_SNAPEND_APPLY_ERROR, SNAPCTL_SNAPEND_PROMOTE_SERVER_ERROR, \
|
|
19
19
|
SNAPCTL_SNAPEND_PROMOTE_TIMEOUT_ERROR, SNAPCTL_SNAPEND_PROMOTE_ERROR, \
|
|
20
20
|
SNAPCTL_SNAPEND_DOWNLOAD_ERROR, SNAPCTL_SNAPEND_UPDATE_TIMEOUT_ERROR, \
|
|
21
|
-
SNAPCTL_SNAPEND_UPDATE_ERROR, SNAPCTL_SNAPEND_STATE_ERROR
|
|
21
|
+
SNAPCTL_SNAPEND_UPDATE_ERROR, SNAPCTL_SNAPEND_STATE_ERROR, HTTP_ERROR_SNAPEND_MANIFEST_MISMATCH, \
|
|
22
|
+
SNAPCTL_SNAPEND_APPLY_MANIFEST_MISMATCH_ERROR
|
|
22
23
|
from snapctl.config.hashes import PROTOS_TYPES, CLIENT_SDK_TYPES, SERVER_SDK_TYPES, \
|
|
23
24
|
SNAPEND_MANIFEST_TYPES, SDK_TYPES, SDK_ACCESS_AUTH_TYPE_LOOKUP
|
|
24
25
|
from snapctl.utils.echo import error, success, info
|
|
@@ -36,7 +37,9 @@ class Snapend:
|
|
|
36
37
|
DOWNLOAD_CATEGORY = [
|
|
37
38
|
'sdk', 'protos', 'snapend-manifest'
|
|
38
39
|
]
|
|
39
|
-
CATEGORY_TYPE_SDK = [
|
|
40
|
+
CATEGORY_TYPE_SDK = [
|
|
41
|
+
# 'omni',
|
|
42
|
+
'user', 'api-key', 'internal', 'app']
|
|
40
43
|
CATEGORY_TYPE_PROTOS = ['messages', 'services']
|
|
41
44
|
# CATEGORY_TYPE_HTTP_LIB_FORMATS = ['unity', 'web-ts', 'ts']
|
|
42
45
|
|
|
@@ -57,6 +60,7 @@ class Snapend:
|
|
|
57
60
|
env: Union[str, None] = None,
|
|
58
61
|
# Clone, Apply, Promote
|
|
59
62
|
manifest_path_filename: Union[str, None] = None,
|
|
63
|
+
force: bool = False,
|
|
60
64
|
# Download
|
|
61
65
|
category: Union[str, None] = None,
|
|
62
66
|
category_format: Union[str, None] = None,
|
|
@@ -78,6 +82,7 @@ class Snapend:
|
|
|
78
82
|
self.name: str = name
|
|
79
83
|
self.env: str = env
|
|
80
84
|
self.manifest_path_filename: Union[str, None] = manifest_path_filename
|
|
85
|
+
self.force: bool = force
|
|
81
86
|
self.category: str = category
|
|
82
87
|
self.portal_category: Union[str, None] = Snapend._make_portal_category(
|
|
83
88
|
category_format)
|
|
@@ -409,26 +414,26 @@ class Snapend:
|
|
|
409
414
|
)
|
|
410
415
|
# Special cases for client SDKs
|
|
411
416
|
if self.category_format in CLIENT_SDK_TYPES:
|
|
412
|
-
if self.category_type in ['
|
|
417
|
+
if self.category_type in ['api-key', 'app', 'internal']:
|
|
413
418
|
snapctl_error(
|
|
414
419
|
message="Invalid combination of format and type. " +
|
|
415
420
|
", ".join(CLIENT_SDK_TYPES.keys()) +
|
|
421
|
+
# " SDKs are only available for --type=omni or --type=user",
|
|
416
422
|
" SDKs are only available for --type=user",
|
|
417
423
|
code=SNAPCTL_INPUT_ERROR
|
|
418
424
|
)
|
|
419
|
-
if self.category_type == 'internal':
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
+
# )
|
|
425
431
|
if self.category_http_lib:
|
|
426
432
|
# First check if the format supports http-lib
|
|
427
433
|
if self.category_format in Snapend.get_formats_supporting_http_lib():
|
|
428
434
|
# Check if the http-lib is supported for the format
|
|
429
435
|
valid_http_libs = SDK_TYPES[self.category_format]['http-lib']
|
|
430
436
|
if self.category_http_lib not in valid_http_libs:
|
|
431
|
-
print('Caught you')
|
|
432
437
|
snapctl_error(
|
|
433
438
|
message="Invalid HTTP Library. Valid libraries are " +
|
|
434
439
|
Snapend.get_http_formats_str(),
|
|
@@ -598,14 +603,17 @@ class Snapend:
|
|
|
598
603
|
payload = {
|
|
599
604
|
'ext': self.manifest_file_name.split('.')[-1]
|
|
600
605
|
}
|
|
606
|
+
if self.force is True:
|
|
607
|
+
info('Force flag is set. Ignoring manifest diff.')
|
|
608
|
+
payload['ignore_diff'] = 'true'
|
|
601
609
|
url = f"{self.base_url}/v1/snapser-api/snapends/snapend-manifest"
|
|
602
610
|
res = requests.put(
|
|
603
611
|
url, headers={'api-key': self.api_key},
|
|
604
612
|
files=files, data=payload, timeout=SERVER_CALL_TIMEOUT
|
|
605
613
|
)
|
|
614
|
+
response = res.json()
|
|
606
615
|
if res.ok:
|
|
607
616
|
# extract the cluster ID
|
|
608
|
-
response = res.json()
|
|
609
617
|
if 'cluster' not in response or 'id' not in response['cluster']:
|
|
610
618
|
snapctl_error(
|
|
611
619
|
message='Server Error. Unable to get a Snapend ID. '
|
|
@@ -643,6 +651,13 @@ class Snapend:
|
|
|
643
651
|
f"{response['cluster']['id']}`",
|
|
644
652
|
progress=progress
|
|
645
653
|
)
|
|
654
|
+
else:
|
|
655
|
+
if "api_error_code" in response and "message" in response:
|
|
656
|
+
if response['api_error_code'] == HTTP_ERROR_SNAPEND_MANIFEST_MISMATCH:
|
|
657
|
+
snapctl_error(
|
|
658
|
+
message='Remote manifest does not match the manifest in the applied_configuration field.',
|
|
659
|
+
code=SNAPCTL_SNAPEND_APPLY_MANIFEST_MISMATCH_ERROR, progress=progress
|
|
660
|
+
)
|
|
646
661
|
except RequestException as e:
|
|
647
662
|
snapctl_error(
|
|
648
663
|
message=f"Unable to apply the manifest snapend. {e}",
|