snapctl 1.0.3__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.

@@ -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/byows.py CHANGED
@@ -20,6 +20,7 @@ from snapctl.config.constants import SERVER_CALL_TIMEOUT, SNAPCTL_INPUT_ERROR, \
20
20
  from snapctl.utils.helper import snapctl_error, snapctl_success, get_dot_snapser_dir
21
21
  from snapctl.utils.echo import info, warning
22
22
 
23
+
23
24
  class Byows:
24
25
  """
25
26
  CLI commands exposed for a Bring your own Workstation
@@ -106,7 +107,7 @@ class Byows:
106
107
  env_vars = []
107
108
 
108
109
  for snap_id in snap_ids:
109
- upper_id = snap_id.upper()
110
+ upper_id = snap_id.upper().replace("-", "_")
110
111
  env_vars.append(f"SNAPEND_{upper_id}_GRPC_URL=localhost:{port}")
111
112
  env_vars.append(
112
113
  f"SNAPEND_{upper_id}_HTTP_URL=http://localhost:{port}")
@@ -128,7 +129,7 @@ class Byows:
128
129
  env_lines = []
129
130
 
130
131
  for snap_id in snap_ids:
131
- upper_id = snap_id.upper()
132
+ upper_id = snap_id.upper().replace("-", "_")
132
133
  env_lines.append(
133
134
  f"export SNAPEND_{upper_id}_GRPC_URL=localhost:{port}")
134
135
  env_lines.append(
@@ -393,7 +394,7 @@ class Byows:
393
394
  self._handle_signal))
394
395
  if hasattr(signal, "SIGBREAK"):
395
396
  signal.signal(signal.SIGBREAK,
396
- functools.partial(self._handle_signal))
397
+ functools.partial(self._handle_signal))
397
398
  # Set up port forward
398
399
  self._setup_port_forward(
399
400
  response_json['proxyPrivateKey'],
@@ -416,7 +417,8 @@ class Byows:
416
417
  code=SNAPCTL_BYOWS_ATTACH_ERROR, progress=progress)
417
418
  except HTTPError as http_err:
418
419
  snapctl_error(
419
- message=Byows._format_portal_http_error("Attach failed.", http_err, res),
420
+ message=Byows._format_portal_http_error(
421
+ "Attach failed.", http_err, res),
420
422
  code=SNAPCTL_BYOWS_ATTACH_ERROR, progress=progress)
421
423
  except RequestException as e:
422
424
  snapctl_error(
snapctl/commands/game.py CHANGED
@@ -1,5 +1,5 @@
1
1
  """
2
- Snapend CLI commands
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 download the SDK {e}",
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 update your snapend {e}",
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()
@@ -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, HTTP_ERROR_SNAPEND_MANIFEST_MISMATCH, \
22
- SNAPCTL_SNAPEND_APPLY_MANIFEST_MISMATCH_ERROR
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
- if res.ok:
281
- content: bytes = res.content
282
- with open(file_save_path, "wb") as file:
283
- if self.category in ['admin-settings']:
284
- content = json.loads(res.content)
285
- json.dump(content, file, indent=4)
286
- else:
287
- file.write(res.content)
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: game_id",
409
+ message="Missing required parameter: application-id",
339
410
  code=SNAPCTL_INPUT_ERROR)
340
- elif self.subcommand == 'clone':
411
+ elif self.subcommand == 'create':
341
412
  if not self.game_id:
342
413
  snapctl_error(
343
- message="Missing required parameter: game_id",
414
+ message="Missing required parameter: application-id",
344
415
  code=SNAPCTL_INPUT_ERROR)
345
- if not self.name:
416
+ if not self.manifest_path_filename:
346
417
  snapctl_error(
347
- message="Missing required parameter: name",
418
+ message="Missing required parameter: manifest_path_filename",
348
419
  code=SNAPCTL_INPUT_ERROR)
349
- if not self.env:
420
+ if not os.path.isfile(self.manifest_path_filename):
350
421
  snapctl_error(
351
- message="Missing required parameter: env", code=SNAPCTL_INPUT_ERROR)
352
- if self.env.upper() not in Snapend.ENV_TYPES:
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 environment. Valid environments are " +
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 clone(self) -> None:
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 apply the manifest.',
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='Remote manifest does not match the manifest in the applied_configuration field.',
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}",