snapctl 1.0.4__py3-none-any.whl → 1.1.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/application.py +135 -0
- snapctl/commands/game.py +3 -3
- snapctl/commands/snapend.py +246 -28
- snapctl/commands/snapend_manifest.py +541 -0
- snapctl/commands/snaps.py +109 -0
- snapctl/config/constants.py +18 -2
- snapctl/data/releases/1.1.0.mdx +20 -0
- snapctl/main.py +157 -10
- snapctl/utils/exceptions.py +8 -0
- snapctl/utils/helper.py +2 -1
- {snapctl-1.0.4.dist-info → snapctl-1.1.0.dist-info}/METADATA +158 -35
- {snapctl-1.0.4.dist-info → snapctl-1.1.0.dist-info}/RECORD +15 -10
- {snapctl-1.0.4.dist-info → snapctl-1.1.0.dist-info}/LICENSE +0 -0
- {snapctl-1.0.4.dist-info → snapctl-1.1.0.dist-info}/WHEEL +0 -0
- {snapctl-1.0.4.dist-info → snapctl-1.1.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Application CLI commands
|
|
3
|
+
"""
|
|
4
|
+
from typing import Union
|
|
5
|
+
import requests
|
|
6
|
+
from requests.exceptions import RequestException
|
|
7
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
8
|
+
from snapctl.config.constants import SERVER_CALL_TIMEOUT, SNAPCTL_INPUT_ERROR, \
|
|
9
|
+
SNAPCTL_GAME_CREATE_ERROR, SNAPCTL_GAME_ENUMERATE_ERROR, \
|
|
10
|
+
HTTP_ERROR_DUPLICATE_GAME_NAME, SNAPCTL_GAME_CREATE_DUPLICATE_NAME_ERROR, \
|
|
11
|
+
HTTP_ERROR_GAME_LIMIT_REACHED, SNAPCTL_GAME_CREATE_LIMIT_ERROR
|
|
12
|
+
from snapctl.utils.helper import snapctl_error, snapctl_success
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Application:
|
|
16
|
+
"""
|
|
17
|
+
CLI commands exposed for an Application
|
|
18
|
+
"""
|
|
19
|
+
SUBCOMMANDS = ['create', 'enumerate']
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self, *, subcommand: str, base_url: str, api_key: Union[str, None],
|
|
23
|
+
name: Union[str, None] = None
|
|
24
|
+
) -> None:
|
|
25
|
+
self.subcommand: str = subcommand
|
|
26
|
+
self.base_url: str = base_url
|
|
27
|
+
self.api_key: Union[str, None] = api_key
|
|
28
|
+
self.name: Union[str, None] = name
|
|
29
|
+
# Validate input
|
|
30
|
+
self.validate_input()
|
|
31
|
+
|
|
32
|
+
def validate_input(self) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Validator
|
|
35
|
+
"""
|
|
36
|
+
# Check API Key and Base URL
|
|
37
|
+
if not self.api_key or self.base_url == '':
|
|
38
|
+
snapctl_error(
|
|
39
|
+
message="Missing API Key.", code=SNAPCTL_INPUT_ERROR)
|
|
40
|
+
# Check subcommand
|
|
41
|
+
if not self.subcommand in Application.SUBCOMMANDS:
|
|
42
|
+
snapctl_error(
|
|
43
|
+
message="Invalid command. Valid commands are " +
|
|
44
|
+
f"{', '.join(Application.SUBCOMMANDS)}.",
|
|
45
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
46
|
+
# Check commands
|
|
47
|
+
if self.subcommand == 'create':
|
|
48
|
+
if self.name is None or self.name == '':
|
|
49
|
+
snapctl_error(
|
|
50
|
+
message="Missing application name.",
|
|
51
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
52
|
+
|
|
53
|
+
def create(self, no_exit: bool = False) -> bool:
|
|
54
|
+
"""
|
|
55
|
+
Create an application
|
|
56
|
+
"""
|
|
57
|
+
progress = Progress(
|
|
58
|
+
SpinnerColumn(),
|
|
59
|
+
TextColumn("[progress.description]{task.description}"),
|
|
60
|
+
transient=True,
|
|
61
|
+
)
|
|
62
|
+
progress.start()
|
|
63
|
+
progress.add_task(
|
|
64
|
+
description='Creating a new application on Snapser...', total=None)
|
|
65
|
+
try:
|
|
66
|
+
url = f"{self.base_url}/v1/snapser-api/games"
|
|
67
|
+
payload = {
|
|
68
|
+
'name': self.name
|
|
69
|
+
}
|
|
70
|
+
res = requests.post(
|
|
71
|
+
url, headers={'api-key': self.api_key},
|
|
72
|
+
json=payload, timeout=SERVER_CALL_TIMEOUT
|
|
73
|
+
)
|
|
74
|
+
if res.ok:
|
|
75
|
+
snapctl_success(
|
|
76
|
+
message=f"Game {self.name} create successful",
|
|
77
|
+
progress=progress, no_exit=no_exit)
|
|
78
|
+
return
|
|
79
|
+
response_json = res.json()
|
|
80
|
+
if "api_error_code" in response_json and "message" in response_json:
|
|
81
|
+
if response_json['api_error_code'] == HTTP_ERROR_GAME_LIMIT_REACHED:
|
|
82
|
+
snapctl_error(
|
|
83
|
+
message=f"Game {self.name} already exists.",
|
|
84
|
+
code=SNAPCTL_GAME_CREATE_LIMIT_ERROR, progress=progress)
|
|
85
|
+
if response_json['api_error_code'] == HTTP_ERROR_DUPLICATE_GAME_NAME:
|
|
86
|
+
snapctl_error(
|
|
87
|
+
message=f"Game {self.name} already exists.",
|
|
88
|
+
code=SNAPCTL_GAME_CREATE_DUPLICATE_NAME_ERROR, progress=progress)
|
|
89
|
+
snapctl_error(
|
|
90
|
+
message=f'Server error: {response_json}',
|
|
91
|
+
code=SNAPCTL_GAME_CREATE_ERROR, progress=progress)
|
|
92
|
+
except RequestException as e:
|
|
93
|
+
snapctl_error(
|
|
94
|
+
message=f"Exception: Unable to download the SDK {e}",
|
|
95
|
+
code=SNAPCTL_GAME_CREATE_ERROR, progress=progress)
|
|
96
|
+
finally:
|
|
97
|
+
progress.stop()
|
|
98
|
+
snapctl_error(
|
|
99
|
+
message='Failed to create an application.',
|
|
100
|
+
code=SNAPCTL_GAME_CREATE_ERROR, progress=progress)
|
|
101
|
+
|
|
102
|
+
def enumerate(self) -> bool:
|
|
103
|
+
"""
|
|
104
|
+
Enumerate all applications
|
|
105
|
+
"""
|
|
106
|
+
progress = Progress(
|
|
107
|
+
SpinnerColumn(),
|
|
108
|
+
TextColumn("[progress.description]{task.description}"),
|
|
109
|
+
transient=True,
|
|
110
|
+
)
|
|
111
|
+
progress.start()
|
|
112
|
+
progress.add_task(
|
|
113
|
+
description='Enumerating all your applications on Snapser...', total=None)
|
|
114
|
+
try:
|
|
115
|
+
url = f"{self.base_url}/v1/snapser-api/games"
|
|
116
|
+
res = requests.get(
|
|
117
|
+
url, headers={'api-key': self.api_key},
|
|
118
|
+
timeout=SERVER_CALL_TIMEOUT
|
|
119
|
+
)
|
|
120
|
+
response_json = res.json()
|
|
121
|
+
if res.ok and 'games' in response_json:
|
|
122
|
+
snapctl_success(
|
|
123
|
+
message=response_json['games'], progress=progress)
|
|
124
|
+
snapctl_error(
|
|
125
|
+
message='Unable to enumerate applications.',
|
|
126
|
+
code=SNAPCTL_GAME_ENUMERATE_ERROR, progress=progress)
|
|
127
|
+
except RequestException as e:
|
|
128
|
+
snapctl_error(
|
|
129
|
+
message=f"Exception: Unable to enumerate applications {e}",
|
|
130
|
+
code=SNAPCTL_GAME_ENUMERATE_ERROR, progress=progress)
|
|
131
|
+
finally:
|
|
132
|
+
progress.stop()
|
|
133
|
+
snapctl_error(
|
|
134
|
+
message='Failed to enumerate applications.',
|
|
135
|
+
code=SNAPCTL_GAME_ENUMERATE_ERROR, progress=progress)
|
snapctl/commands/game.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Game CLI commands
|
|
3
3
|
"""
|
|
4
4
|
from typing import Union
|
|
5
5
|
import requests
|
|
@@ -91,7 +91,7 @@ class Game:
|
|
|
91
91
|
code=SNAPCTL_GAME_CREATE_ERROR, progress=progress)
|
|
92
92
|
except RequestException as e:
|
|
93
93
|
snapctl_error(
|
|
94
|
-
message=f"Exception: Unable to
|
|
94
|
+
message=f"Exception: Unable to create game {e}",
|
|
95
95
|
code=SNAPCTL_GAME_CREATE_ERROR, progress=progress)
|
|
96
96
|
finally:
|
|
97
97
|
progress.stop()
|
|
@@ -126,7 +126,7 @@ class Game:
|
|
|
126
126
|
code=SNAPCTL_GAME_ENUMERATE_ERROR, progress=progress)
|
|
127
127
|
except RequestException as e:
|
|
128
128
|
snapctl_error(
|
|
129
|
-
message=f"Exception: Unable to
|
|
129
|
+
message=f"Exception: Unable to enumerate games {e}",
|
|
130
130
|
code=SNAPCTL_GAME_ENUMERATE_ERROR, progress=progress)
|
|
131
131
|
finally:
|
|
132
132
|
progress.stop()
|
snapctl/commands/snapend.py
CHANGED
|
@@ -18,12 +18,16 @@ 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,
|
|
22
|
-
|
|
21
|
+
SNAPCTL_SNAPEND_UPDATE_ERROR, SNAPCTL_SNAPEND_STATE_ERROR, \
|
|
22
|
+
HTTP_ERROR_SNAPEND_MANIFEST_MISMATCH, HTTP_ERROR_CLUSTER_UPDATE_IN_PROGRESS, \
|
|
23
|
+
SNAPCTL_SNAPEND_APPLY_MANIFEST_MISMATCH_ERROR, HTTP_ERROR_GAME_NOT_FOUND, \
|
|
24
|
+
SNAPCTL_SNAPEND_CREATE_SERVER_ERROR, SNAPCTL_SNAPEND_CREATE_TIMEOUT_ERROR, \
|
|
25
|
+
SNAPCTL_SNAPEND_CREATE_ERROR, HTTP_CONFLICT
|
|
23
26
|
from snapctl.config.hashes import PROTOS_TYPES, CLIENT_SDK_TYPES, SERVER_SDK_TYPES, \
|
|
24
27
|
SNAPEND_MANIFEST_TYPES, SDK_TYPES, SDK_ACCESS_AUTH_TYPE_LOOKUP
|
|
25
28
|
from snapctl.utils.echo import error, success, info
|
|
26
29
|
from snapctl.utils.helper import snapctl_error, snapctl_success
|
|
30
|
+
from snapctl.utils.exceptions import SnapendDownloadException
|
|
27
31
|
|
|
28
32
|
|
|
29
33
|
class Snapend:
|
|
@@ -31,7 +35,7 @@ class Snapend:
|
|
|
31
35
|
CLI commands exposed for a Snapend
|
|
32
36
|
"""
|
|
33
37
|
SUBCOMMANDS = [
|
|
34
|
-
'enumerate', 'clone', 'apply',
|
|
38
|
+
'enumerate', 'create', 'clone', 'apply',
|
|
35
39
|
'download', 'update', 'state'
|
|
36
40
|
]
|
|
37
41
|
DOWNLOAD_CATEGORY = [
|
|
@@ -79,8 +83,8 @@ class Snapend:
|
|
|
79
83
|
self.api_key: str = api_key
|
|
80
84
|
self.snapend_id: str = snapend_id
|
|
81
85
|
self.game_id: Union[str, None] = game_id
|
|
82
|
-
self.name: str = name
|
|
83
|
-
self.env: str = env
|
|
86
|
+
self.name: Union[str, None] = name
|
|
87
|
+
self.env: Union[str, None] = env
|
|
84
88
|
self.manifest_path_filename: Union[str, None] = manifest_path_filename
|
|
85
89
|
self.force: bool = force
|
|
86
90
|
self.category: str = category
|
|
@@ -103,6 +107,9 @@ class Snapend:
|
|
|
103
107
|
self.byogs_list: Union[str, None] = Snapend._make_byogs_list(
|
|
104
108
|
byogs) if byogs else None
|
|
105
109
|
self.blocking: bool = blocking
|
|
110
|
+
# Backup variables
|
|
111
|
+
self.manifest_name: Union[str, None] = None
|
|
112
|
+
self.manifest_environment: Union[str, None] = None
|
|
106
113
|
# Validate input
|
|
107
114
|
self.validate_input()
|
|
108
115
|
|
|
@@ -252,6 +259,18 @@ class Snapend:
|
|
|
252
259
|
res = requests.get(
|
|
253
260
|
url, headers={'api-key': self.api_key}, timeout=SERVER_CALL_TIMEOUT
|
|
254
261
|
)
|
|
262
|
+
if not res.ok:
|
|
263
|
+
# Handle known conflict case
|
|
264
|
+
if res.status_code == HTTP_CONFLICT:
|
|
265
|
+
response = res.json()
|
|
266
|
+
if (
|
|
267
|
+
response.get(
|
|
268
|
+
"api_error_code") == HTTP_ERROR_CLUSTER_UPDATE_IN_PROGRESS
|
|
269
|
+
):
|
|
270
|
+
raise SnapendDownloadException(
|
|
271
|
+
"Snapend update is in progress. Please try again later.")
|
|
272
|
+
return False
|
|
273
|
+
|
|
255
274
|
fn: str = ''
|
|
256
275
|
if self.category == 'admin-settings':
|
|
257
276
|
fn = f"snapser-{self.snapend_id}-admin-settings.json"
|
|
@@ -277,15 +296,14 @@ class Snapend:
|
|
|
277
296
|
file_save_path = os.path.join(self.out_path, fn)
|
|
278
297
|
else:
|
|
279
298
|
file_save_path = os.path.join(os.getcwd(), fn)
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
return True
|
|
299
|
+
|
|
300
|
+
with open(file_save_path, "wb") as file:
|
|
301
|
+
if self.category in ['admin-settings']:
|
|
302
|
+
content = json.loads(res.content)
|
|
303
|
+
json.dump(content, file, indent=4)
|
|
304
|
+
else:
|
|
305
|
+
file.write(res.content)
|
|
306
|
+
return True
|
|
289
307
|
except RequestException as e:
|
|
290
308
|
info(
|
|
291
309
|
f"Exception: Unable to download {self.category}. Reason: {e}"
|
|
@@ -316,6 +334,59 @@ class Snapend:
|
|
|
316
334
|
format_str = format_str[:-2]
|
|
317
335
|
return format_str
|
|
318
336
|
|
|
337
|
+
# Backup variables
|
|
338
|
+
|
|
339
|
+
def setup_manifest_name_and_env_vars(self) -> bool:
|
|
340
|
+
"""
|
|
341
|
+
Read a manifest (JSON or YAML) and saves the name and environment.
|
|
342
|
+
Supports extensions: .json, .yaml, .yml
|
|
343
|
+
If the extension is unknown, tries JSON then YAML.
|
|
344
|
+
"""
|
|
345
|
+
def parse_json(s: str):
|
|
346
|
+
return json.loads(s)
|
|
347
|
+
|
|
348
|
+
def parse_yaml(s: str):
|
|
349
|
+
try:
|
|
350
|
+
import yaml # type: ignore
|
|
351
|
+
except ImportError as e:
|
|
352
|
+
raise RuntimeError(
|
|
353
|
+
"YAML file provided but PyYAML is not installed. "
|
|
354
|
+
"Install with: pip install pyyaml"
|
|
355
|
+
) from e
|
|
356
|
+
return yaml.safe_load(s)
|
|
357
|
+
|
|
358
|
+
with open(self.manifest_path_filename, "r", encoding="utf-8") as f:
|
|
359
|
+
text = f.read()
|
|
360
|
+
|
|
361
|
+
ext = os.path.splitext(self.manifest_path_filename)[1].lower()
|
|
362
|
+
if ext == ".json":
|
|
363
|
+
parsers = (parse_json, parse_yaml)
|
|
364
|
+
elif ext in (".yaml", ".yml"):
|
|
365
|
+
parsers = (parse_yaml, parse_json)
|
|
366
|
+
else:
|
|
367
|
+
parsers = (parse_json, parse_yaml)
|
|
368
|
+
|
|
369
|
+
last_err = None
|
|
370
|
+
data = None
|
|
371
|
+
for parser in parsers:
|
|
372
|
+
try:
|
|
373
|
+
data = parser(text)
|
|
374
|
+
break
|
|
375
|
+
except Exception as e:
|
|
376
|
+
last_err = e
|
|
377
|
+
|
|
378
|
+
if data is None:
|
|
379
|
+
return False
|
|
380
|
+
if not isinstance(data, dict):
|
|
381
|
+
return False
|
|
382
|
+
|
|
383
|
+
try:
|
|
384
|
+
self.manifest_name = str(data["name"])
|
|
385
|
+
self.manifest_environment = str(data["environment"])
|
|
386
|
+
except KeyError as e:
|
|
387
|
+
pass
|
|
388
|
+
return False
|
|
389
|
+
|
|
319
390
|
# Validate input
|
|
320
391
|
def validate_input(self) -> None:
|
|
321
392
|
"""
|
|
@@ -335,26 +406,51 @@ class Snapend:
|
|
|
335
406
|
if self.subcommand == 'enumerate':
|
|
336
407
|
if not self.game_id:
|
|
337
408
|
snapctl_error(
|
|
338
|
-
message="Missing required parameter:
|
|
409
|
+
message="Missing required parameter: application-id",
|
|
339
410
|
code=SNAPCTL_INPUT_ERROR)
|
|
340
|
-
elif self.subcommand == '
|
|
411
|
+
elif self.subcommand == 'create':
|
|
341
412
|
if not self.game_id:
|
|
342
413
|
snapctl_error(
|
|
343
|
-
message="Missing required parameter:
|
|
414
|
+
message="Missing required parameter: application-id",
|
|
344
415
|
code=SNAPCTL_INPUT_ERROR)
|
|
345
|
-
if not self.
|
|
416
|
+
if not self.manifest_path_filename:
|
|
346
417
|
snapctl_error(
|
|
347
|
-
message="Missing required parameter:
|
|
418
|
+
message="Missing required parameter: manifest_path_filename",
|
|
348
419
|
code=SNAPCTL_INPUT_ERROR)
|
|
349
|
-
if not self.
|
|
420
|
+
if not os.path.isfile(self.manifest_path_filename):
|
|
350
421
|
snapctl_error(
|
|
351
|
-
message="
|
|
352
|
-
|
|
422
|
+
message=f"Invalid path {self.manifest_path_filename}. " +
|
|
423
|
+
"Please enter a valid path to the manifest file",
|
|
424
|
+
code=SNAPCTL_INPUT_ERROR
|
|
425
|
+
)
|
|
426
|
+
if not self.manifest_file_name:
|
|
353
427
|
snapctl_error(
|
|
354
|
-
message="Invalid
|
|
428
|
+
message="Invalid manifest file. Supported formats are .json, .yml, .yaml",
|
|
429
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
430
|
+
self.setup_manifest_name_and_env_vars()
|
|
431
|
+
if not self.name and not self.manifest_name:
|
|
432
|
+
snapctl_error(
|
|
433
|
+
message="Missing name parameter or name in manifest is required for " +
|
|
434
|
+
"create command.",
|
|
435
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
436
|
+
if not self.env and not self.manifest_environment:
|
|
437
|
+
snapctl_error(
|
|
438
|
+
message="Missing environment parameter or environment in manifest is " +
|
|
439
|
+
"required for create command.",
|
|
440
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
441
|
+
if self.env and self.env.upper() not in Snapend.ENV_TYPES and \
|
|
442
|
+
self.manifest_environment not in Snapend.ENV_TYPES:
|
|
443
|
+
env_str = 'Invalid environment argument.' if not self.env else 'Invalid environment in manifest.'
|
|
444
|
+
snapctl_error(
|
|
445
|
+
message=f"{env_str}. Valid environments are " +
|
|
355
446
|
f"{', '.join(Snapend.ENV_TYPES)}.",
|
|
356
447
|
code=SNAPCTL_INPUT_ERROR
|
|
357
448
|
)
|
|
449
|
+
elif self.subcommand == 'clone':
|
|
450
|
+
if not self.game_id:
|
|
451
|
+
snapctl_error(
|
|
452
|
+
message="Missing required parameter: application-id",
|
|
453
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
358
454
|
if not self.manifest_path_filename:
|
|
359
455
|
snapctl_error(
|
|
360
456
|
message="Missing required parameter: manifest_path_filename",
|
|
@@ -365,6 +461,29 @@ class Snapend:
|
|
|
365
461
|
"Please enter a valid path to the manifest file",
|
|
366
462
|
code=SNAPCTL_INPUT_ERROR
|
|
367
463
|
)
|
|
464
|
+
if not self.manifest_file_name:
|
|
465
|
+
snapctl_error(
|
|
466
|
+
message="Invalid manifest file. Supported formats are .json, .yml, .yaml",
|
|
467
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
468
|
+
self.setup_manifest_name_and_env_vars()
|
|
469
|
+
if not self.name and not self.manifest_name:
|
|
470
|
+
snapctl_error(
|
|
471
|
+
message="Missing name parameter or name in manifest is required for " +
|
|
472
|
+
"create command.",
|
|
473
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
474
|
+
if not self.env and not self.manifest_environment:
|
|
475
|
+
snapctl_error(
|
|
476
|
+
message="Missing environment parameter or environment in manifest is " +
|
|
477
|
+
"required for create command.",
|
|
478
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
479
|
+
if self.env and self.env.upper() not in Snapend.ENV_TYPES and \
|
|
480
|
+
self.manifest_environment not in Snapend.ENV_TYPES:
|
|
481
|
+
env_str = 'Invalid environment argument.' if not self.env else 'Invalid environment in manifest.'
|
|
482
|
+
snapctl_error(
|
|
483
|
+
message=f"{env_str}. Valid environments are " +
|
|
484
|
+
f"{', '.join(Snapend.ENV_TYPES)}.",
|
|
485
|
+
code=SNAPCTL_INPUT_ERROR
|
|
486
|
+
)
|
|
368
487
|
elif self.subcommand == 'apply':
|
|
369
488
|
if not self.manifest_path_filename:
|
|
370
489
|
snapctl_error(
|
|
@@ -516,7 +635,7 @@ class Snapend:
|
|
|
516
635
|
message="Unable to enumerate snapends.",
|
|
517
636
|
code=SNAPCTL_SNAPEND_ENUMERATE_ERROR, progress=progress)
|
|
518
637
|
|
|
519
|
-
def
|
|
638
|
+
def create(self) -> None:
|
|
520
639
|
"""
|
|
521
640
|
Create a Snapend from a manifest
|
|
522
641
|
"""
|
|
@@ -526,6 +645,84 @@ class Snapend:
|
|
|
526
645
|
transient=True,
|
|
527
646
|
)
|
|
528
647
|
progress.start()
|
|
648
|
+
progress.add_task(
|
|
649
|
+
description='Creating your snapend from the manifest...', total=None)
|
|
650
|
+
try:
|
|
651
|
+
with open(self.manifest_path_filename, 'rb') as file:
|
|
652
|
+
files = {'snapend-manifest': file}
|
|
653
|
+
payload = {
|
|
654
|
+
'game_id': self.game_id,
|
|
655
|
+
'name': self.name if self.name else self.manifest_name,
|
|
656
|
+
'env': self.env.upper() if self.env else self.manifest_environment.upper(),
|
|
657
|
+
'ext': self.manifest_file_name.split('.')[-1]
|
|
658
|
+
}
|
|
659
|
+
url = f"{self.base_url}/v1/snapser-api/snapends/snapend-manifest"
|
|
660
|
+
res = requests.post(
|
|
661
|
+
url, headers={'api-key': self.api_key},
|
|
662
|
+
files=files, data=payload, timeout=SERVER_CALL_TIMEOUT
|
|
663
|
+
)
|
|
664
|
+
if res.ok:
|
|
665
|
+
# extract the cluster ID
|
|
666
|
+
response = res.json()
|
|
667
|
+
if 'cluster' not in response or 'id' not in response['cluster']:
|
|
668
|
+
snapctl_error(
|
|
669
|
+
message='Server Error. Unable to get a Snapend ID. ' +
|
|
670
|
+
'Please try again in sometime.',
|
|
671
|
+
code=SNAPCTL_SNAPEND_CREATE_SERVER_ERROR,
|
|
672
|
+
progress=progress
|
|
673
|
+
)
|
|
674
|
+
self._assign_snapend_id(response['cluster']['id'])
|
|
675
|
+
info(f"Cluster ID assigned: {response['cluster']['id']}")
|
|
676
|
+
if self.blocking:
|
|
677
|
+
snapctl_success(
|
|
678
|
+
message='Snapend create initiated.',
|
|
679
|
+
progress=progress,
|
|
680
|
+
no_exit=True
|
|
681
|
+
)
|
|
682
|
+
status = self._blocking_get_status()
|
|
683
|
+
# Fetch the new manifest
|
|
684
|
+
if status is True:
|
|
685
|
+
# TODO: Uncomment this if we want to do an auto download
|
|
686
|
+
# self._setup_for_download(
|
|
687
|
+
# self.manifest_file_name.split('.')[-1])
|
|
688
|
+
# self._execute_download()
|
|
689
|
+
snapctl_success(
|
|
690
|
+
message='Snapend create successful. Do not forget to download the latest manifest.',
|
|
691
|
+
progress=progress)
|
|
692
|
+
snapctl_error(
|
|
693
|
+
message='Snapend create has been initiated but the Snapend is not up yet.' +
|
|
694
|
+
'Please try checking the status of the Snapend in some time',
|
|
695
|
+
code=SNAPCTL_SNAPEND_CREATE_TIMEOUT_ERROR,
|
|
696
|
+
progress=progress
|
|
697
|
+
)
|
|
698
|
+
snapctl_success(
|
|
699
|
+
message="Snapend create has been initiated. " +
|
|
700
|
+
"You can check the status using " +
|
|
701
|
+
"`snapctl snapend state --snapend-id" +
|
|
702
|
+
f"{response['cluster']['id']}`",
|
|
703
|
+
progress=progress
|
|
704
|
+
)
|
|
705
|
+
except RequestException as e:
|
|
706
|
+
snapctl_error(
|
|
707
|
+
message=f"Unable to create a snapend from a manifest. {e}",
|
|
708
|
+
code=SNAPCTL_SNAPEND_CREATE_ERROR, progress=progress
|
|
709
|
+
)
|
|
710
|
+
finally:
|
|
711
|
+
progress.stop()
|
|
712
|
+
snapctl_error(
|
|
713
|
+
message='Unable to create a snapend from a manifest.',
|
|
714
|
+
code=SNAPCTL_SNAPEND_CREATE_ERROR, progress=progress)
|
|
715
|
+
|
|
716
|
+
def clone(self) -> None:
|
|
717
|
+
"""
|
|
718
|
+
Clone a Snapend from a manifest
|
|
719
|
+
"""
|
|
720
|
+
progress = Progress(
|
|
721
|
+
SpinnerColumn(),
|
|
722
|
+
TextColumn("[progress.description]{task.description}"),
|
|
723
|
+
transient=True,
|
|
724
|
+
)
|
|
725
|
+
progress.start()
|
|
529
726
|
progress.add_task(
|
|
530
727
|
description='Cloning your snapend from the manifest...', total=None)
|
|
531
728
|
try:
|
|
@@ -533,8 +730,8 @@ class Snapend:
|
|
|
533
730
|
files = {'snapend-manifest': file}
|
|
534
731
|
payload = {
|
|
535
732
|
'game_id': self.game_id,
|
|
536
|
-
'name': self.name,
|
|
537
|
-
'env': self.env.upper(),
|
|
733
|
+
'name': self.name if self.name else self.manifest_name,
|
|
734
|
+
'env': self.env.upper() if self.env else self.manifest_environment.upper(),
|
|
538
735
|
'ext': self.manifest_file_name.split('.')[-1]
|
|
539
736
|
}
|
|
540
737
|
url = f"{self.base_url}/v1/snapser-api/snapends/snapend-manifest"
|
|
@@ -591,7 +788,7 @@ class Snapend:
|
|
|
591
788
|
finally:
|
|
592
789
|
progress.stop()
|
|
593
790
|
snapctl_error(
|
|
594
|
-
message='Unable to
|
|
791
|
+
message='Unable to clone from the manifest.',
|
|
595
792
|
code=SNAPCTL_SNAPEND_CLONE_ERROR, progress=progress)
|
|
596
793
|
|
|
597
794
|
def apply(self) -> None:
|
|
@@ -663,10 +860,25 @@ class Snapend:
|
|
|
663
860
|
else:
|
|
664
861
|
if "api_error_code" in response and "message" in response:
|
|
665
862
|
if response['api_error_code'] == HTTP_ERROR_SNAPEND_MANIFEST_MISMATCH:
|
|
863
|
+
msg = 'Remote manifest does not match the manifest in the ' + \
|
|
864
|
+
'applied_configuration field.'
|
|
666
865
|
snapctl_error(
|
|
667
|
-
message=
|
|
866
|
+
message=msg,
|
|
668
867
|
code=SNAPCTL_SNAPEND_APPLY_MANIFEST_MISMATCH_ERROR, progress=progress
|
|
669
868
|
)
|
|
869
|
+
elif response['api_error_code'] == HTTP_ERROR_GAME_NOT_FOUND:
|
|
870
|
+
msg = 'Game not found. Did you mean to use the clone command to ' + \
|
|
871
|
+
'create a new snapend?'
|
|
872
|
+
snapctl_error(
|
|
873
|
+
message=msg,
|
|
874
|
+
code=SNAPCTL_SNAPEND_APPLY_ERROR, progress=progress
|
|
875
|
+
)
|
|
876
|
+
elif response['api_error_code'] == HTTP_ERROR_CLUSTER_UPDATE_IN_PROGRESS:
|
|
877
|
+
msg = 'Snapend update is in progress. Please try again later.'
|
|
878
|
+
snapctl_error(
|
|
879
|
+
message=msg,
|
|
880
|
+
code=SNAPCTL_SNAPEND_APPLY_ERROR, progress=progress
|
|
881
|
+
)
|
|
670
882
|
except RequestException as e:
|
|
671
883
|
snapctl_error(
|
|
672
884
|
message=f"Unable to apply the manifest snapend. {e}",
|
|
@@ -775,6 +987,12 @@ class Snapend:
|
|
|
775
987
|
f"{self.category} saved at {self.out_path}",
|
|
776
988
|
progress=progress
|
|
777
989
|
)
|
|
990
|
+
except SnapendDownloadException as e:
|
|
991
|
+
snapctl_error(
|
|
992
|
+
message=str(e),
|
|
993
|
+
code=SNAPCTL_SNAPEND_DOWNLOAD_ERROR,
|
|
994
|
+
progress=progress
|
|
995
|
+
)
|
|
778
996
|
except RequestException as e:
|
|
779
997
|
snapctl_error(
|
|
780
998
|
message=f"Unable to download {self.category}. Reason: {e}",
|