snapctl 1.0.4__py3-none-any.whl → 1.1.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.

@@ -0,0 +1,861 @@
1
+ """
2
+ Snapend manifest CLI commands
3
+ """
4
+ import os
5
+ from typing import Union
6
+ import json
7
+ from requests.exceptions import RequestException
8
+ from rich.progress import Progress, SpinnerColumn, TextColumn
9
+ from snapctl.config.constants import SNAPCTL_SNAPEND_MANIFEST_UPGRADE_ERROR, SNAPCTL_INPUT_ERROR, \
10
+ SNAPCTL_SNAPEND_MANIFEST_CREATE_ERROR, SNAPCTL_SNAPEND_MANIFEST_UPDATE_ERROR, \
11
+ SNAPCTL_INTERNAL_SERVER_ERROR, SNAPCTL_SNAPEND_MANIFEST_SYNC_ERROR
12
+ from snapctl.commands.snaps import Snaps
13
+ from snapctl.utils.helper import snapctl_error, snapctl_success, check_duplicates_in_list
14
+ from snapctl.utils.echo import info, warning, success
15
+
16
+
17
+ class SnapendManifest:
18
+ """
19
+ CLI commands exposed for Snapend manifest
20
+ """
21
+ SUBCOMMANDS = ['create', 'sync', 'update', 'upgrade']
22
+ ENVIRONMENTS = ['DEVELOPMENT', 'STAGING', 'PRODUCTION']
23
+ FEATURES = ['WEB_SOCKETS']
24
+ AUTH_SNAP_ID = 'auth'
25
+
26
+ def __init__(
27
+ self, *, subcommand: str, base_url: str, api_key: Union[str, None],
28
+ name: str = 'my-snapend',
29
+ environment: str = 'DEVELOPMENT',
30
+ manifest_path_filename: Union[str, None] = None,
31
+ snaps: Union[str, None] = None,
32
+ features: Union[str, None] = None,
33
+ add_snaps: Union[str, None] = None,
34
+ remove_snaps: Union[str, None] = None,
35
+ add_features: Union[str, None] = None,
36
+ remove_features: Union[str, None] = None,
37
+ out_path_filename: Union[str, None] = None,
38
+ ) -> None:
39
+ self.subcommand: str = subcommand
40
+ self.base_url: str = base_url
41
+ self.api_key: Union[str, None] = api_key
42
+ self.name: str = name
43
+ self.environment: str = environment
44
+ self.manifest_path_filename: Union[str, None] = manifest_path_filename
45
+ self.manifest: Union[dict, None] = None
46
+ self.out_path_filename: Union[str, None] = out_path_filename
47
+ self.snaps = snaps
48
+ self.features = features
49
+ self.add_snaps = add_snaps
50
+ self.remove_snaps = remove_snaps
51
+ self.add_features = add_features
52
+ self.remove_features = remove_features
53
+ self.remote_snaps: list = self.load_snaps()
54
+ # Setup
55
+ self.setup_manifest()
56
+ # Validate input
57
+ self.validate_input()
58
+
59
+ def setup_manifest(self) -> bool:
60
+ """
61
+ Read a manifest (JSON or YAML) and saves it
62
+ Supports extensions: .json, .yaml, .yml
63
+ If the extension is unknown, tries JSON then YAML.
64
+ """
65
+ def parse_json(s: str):
66
+ return json.loads(s)
67
+
68
+ def parse_yaml(s: str):
69
+ try:
70
+ import yaml # type: ignore
71
+ except ImportError as e:
72
+ raise RuntimeError(
73
+ "YAML file provided but PyYAML is not installed. "
74
+ "Install with: pip install pyyaml"
75
+ ) from e
76
+ return yaml.safe_load(s)
77
+
78
+ if not self.manifest_path_filename:
79
+ return False
80
+ with open(self.manifest_path_filename, "r", encoding="utf-8") as f:
81
+ text = f.read()
82
+
83
+ ext = os.path.splitext(self.manifest_path_filename)[1].lower()
84
+ if ext == ".json":
85
+ parsers = (parse_json, parse_yaml)
86
+ elif ext in (".yaml", ".yml"):
87
+ parsers = (parse_yaml, parse_json)
88
+ else:
89
+ parsers = (parse_json, parse_yaml)
90
+
91
+ last_err = None
92
+ data = None
93
+ for parser in parsers:
94
+ try:
95
+ data = parser(text)
96
+ break
97
+ except Exception as e:
98
+ last_err = e
99
+
100
+ if data is None:
101
+ return False
102
+ if not isinstance(data, dict):
103
+ return False
104
+
105
+ try:
106
+ self.manifest = data
107
+ except KeyError as e:
108
+ pass
109
+ return False
110
+
111
+ def load_snaps(self) -> list:
112
+ """
113
+ Load snaps from the Snapser portal
114
+ """
115
+ snaps_response = Snaps.get_snaps(self.base_url, self.api_key)
116
+ if 'services' in snaps_response:
117
+ return snaps_response['services']
118
+ return []
119
+
120
+ def validate_input(self) -> None:
121
+ """
122
+ Validator
123
+ """
124
+ # Check API Key and Base URL
125
+ if not self.api_key or self.base_url == '':
126
+ snapctl_error(
127
+ message="Missing API Key.", code=SNAPCTL_INPUT_ERROR)
128
+ # Check subcommand
129
+ if not self.subcommand in SnapendManifest.SUBCOMMANDS:
130
+ snapctl_error(
131
+ message="Invalid command. Valid commands are " +
132
+ f"{', '.join(SnapendManifest.SUBCOMMANDS)}.",
133
+ code=SNAPCTL_INPUT_ERROR)
134
+ if len(self.remote_snaps) == 0:
135
+ snapctl_error(
136
+ message="Something went wrong. No snaps found. Please try again in some time.",
137
+ code=SNAPCTL_INTERNAL_SERVER_ERROR)
138
+ if self.subcommand == 'create':
139
+ if not self.name or not self.environment:
140
+ snapctl_error(
141
+ message="Name and environment are required for create command.",
142
+ code=SNAPCTL_INPUT_ERROR)
143
+ if self.environment not in SnapendManifest.ENVIRONMENTS:
144
+ snapctl_error(
145
+ message="Environment must be one of " +
146
+ f"{', '.join(SnapendManifest.ENVIRONMENTS)}.",
147
+ code=SNAPCTL_INPUT_ERROR)
148
+ if (not self.snaps or self.snaps == '') and \
149
+ (not self.features or self.features == ''):
150
+ snapctl_error(
151
+ message="At least one of snaps or features " +
152
+ "is required to sync a snapend manifest.",
153
+ code=SNAPCTL_INPUT_ERROR)
154
+ if not self.out_path_filename:
155
+ snapctl_error(
156
+ message="Output path is required for create command.",
157
+ code=SNAPCTL_INPUT_ERROR)
158
+ if self.out_path_filename and not (self.out_path_filename.endswith('.json') or
159
+ self.out_path_filename.endswith('.yaml') or
160
+ self.out_path_filename.endswith('.yml')):
161
+ snapctl_error(
162
+ message="Output path must end with .json, .yaml or .yml",
163
+ code=SNAPCTL_INPUT_ERROR)
164
+ if self.snaps and self.snaps != '':
165
+ input_snaps_list = self.snaps.split(',')
166
+ repeat_snaps = check_duplicates_in_list(input_snaps_list)
167
+ if len(repeat_snaps) > 0:
168
+ snapctl_error(
169
+ message="Duplicate snaps found in input: " +
170
+ f"{', '.join(repeat_snaps)}. Please check and try again.",
171
+ code=SNAPCTL_INPUT_ERROR)
172
+ remote_snaps_ids = [snap['id'] for snap in self.remote_snaps]
173
+ for input_snap in input_snaps_list:
174
+ if not input_snap in remote_snaps_ids:
175
+ snapctl_error(
176
+ message="Invalid Snap " + input_snap +
177
+ " provided with --snaps. Please check and try again.",
178
+ code=SNAPCTL_INPUT_ERROR)
179
+ if self.features:
180
+ input_features_list = self.features.split(',')
181
+ repeat_features = check_duplicates_in_list(input_features_list)
182
+ if len(repeat_features) > 0:
183
+ snapctl_error(
184
+ message="Duplicate features found in input: " +
185
+ f"{', '.join(repeat_features)}. Please check and try again.",
186
+ code=SNAPCTL_INPUT_ERROR)
187
+ for feature in input_features_list:
188
+ feature = feature.strip()
189
+ if feature.upper() not in SnapendManifest.FEATURES:
190
+ snapctl_error(
191
+ message="--features must be one of " +
192
+ f"{', '.join(SnapendManifest.FEATURES)}.",
193
+ code=SNAPCTL_INPUT_ERROR)
194
+ elif self.subcommand == 'sync':
195
+ if not self.manifest_path_filename:
196
+ snapctl_error(
197
+ message="Manifest path is required for sync command.",
198
+ code=SNAPCTL_INPUT_ERROR)
199
+ if (not self.snaps or self.snaps == '') and \
200
+ (not self.features or self.features == ''):
201
+ snapctl_error(
202
+ message="At least one of snaps or features " +
203
+ "is required to sync a snapend manifest.",
204
+ code=SNAPCTL_INPUT_ERROR)
205
+ if self.snaps and self.snaps != '':
206
+ input_snaps_list = self.snaps.split(',')
207
+ repeat_snaps = check_duplicates_in_list(input_snaps_list)
208
+ if len(repeat_snaps) > 0:
209
+ snapctl_error(
210
+ message="Duplicate snaps found in input: " +
211
+ f"{', '.join(repeat_snaps)}. Please check and try again.",
212
+ code=SNAPCTL_INPUT_ERROR)
213
+ remote_snaps_ids = [snap['id'] for snap in self.remote_snaps]
214
+ for input_snap in input_snaps_list:
215
+ if not input_snap in remote_snaps_ids:
216
+ snapctl_error(
217
+ message="Invalid Snap " + input_snap +
218
+ " provided with --snaps. Please check and try again.",
219
+ code=SNAPCTL_INPUT_ERROR)
220
+ if self.features:
221
+ input_features_list = self.features.split(',')
222
+ repeat_features = check_duplicates_in_list(input_features_list)
223
+ if len(repeat_features) > 0:
224
+ snapctl_error(
225
+ message="Duplicate features found in input: " +
226
+ f"{', '.join(repeat_features)}. Please check and try again.",
227
+ code=SNAPCTL_INPUT_ERROR)
228
+ for feature in input_features_list:
229
+ feature = feature.strip()
230
+ if feature.upper() not in SnapendManifest.FEATURES:
231
+ snapctl_error(
232
+ message="--features must be one of " +
233
+ f"{', '.join(SnapendManifest.FEATURES)}.",
234
+ code=SNAPCTL_INPUT_ERROR)
235
+ if not self.out_path_filename:
236
+ snapctl_error(
237
+ message="Output path is required for sync command.",
238
+ code=SNAPCTL_INPUT_ERROR)
239
+ if self.out_path_filename and not (self.out_path_filename.endswith('.json') or
240
+ self.out_path_filename.endswith('.yaml') or
241
+ self.out_path_filename.endswith('.yml')):
242
+ snapctl_error(
243
+ message="Output path must end with .json, .yaml or .yml",
244
+ code=SNAPCTL_INPUT_ERROR)
245
+ if not self.manifest:
246
+ snapctl_error(
247
+ message="Unable to read the manifest file. " +
248
+ "Please check the file and try again.",
249
+ code=SNAPCTL_INPUT_ERROR)
250
+ if 'service_definitions' not in self.manifest:
251
+ snapctl_error(
252
+ message="Invalid manifest file. Need service_definitions. " +
253
+ "Please check the file and try again.",
254
+ code=SNAPCTL_INPUT_ERROR)
255
+ elif self.subcommand == 'upgrade':
256
+ if not self.manifest_path_filename:
257
+ snapctl_error(
258
+ message="Manifest path is required for upgrade command.",
259
+ code=SNAPCTL_INPUT_ERROR)
260
+ if self.snaps and self.snaps != '':
261
+ input_snaps_list = self.snaps.split(',')
262
+ repeat_snaps = check_duplicates_in_list(input_snaps_list)
263
+ if len(repeat_snaps) > 0:
264
+ snapctl_error(
265
+ message="Duplicate snaps found in input: " +
266
+ f"{', '.join(repeat_snaps)}. Please check and try again.",
267
+ code=SNAPCTL_INPUT_ERROR)
268
+ if not self.out_path_filename:
269
+ snapctl_error(
270
+ message="Output path is required for upgrade command.",
271
+ code=SNAPCTL_INPUT_ERROR)
272
+ if self.out_path_filename and not (self.out_path_filename.endswith('.json') or
273
+ self.out_path_filename.endswith('.yaml') or
274
+ self.out_path_filename.endswith('.yml')):
275
+ snapctl_error(
276
+ message="Output path must end with .json, .yaml or .yml",
277
+ code=SNAPCTL_INPUT_ERROR)
278
+ if not self.manifest:
279
+ snapctl_error(
280
+ message="Unable to read the manifest file. " +
281
+ "Please check the file and try again.",
282
+ code=SNAPCTL_INPUT_ERROR)
283
+ if 'service_definitions' not in self.manifest:
284
+ snapctl_error(
285
+ message="Invalid manifest file. Need service_definitions. " +
286
+ "Please check the file and try again.",
287
+ code=SNAPCTL_INPUT_ERROR)
288
+ if self.snaps and self.snaps != '':
289
+ input_snaps_list = self.snaps.split(',')
290
+ remote_snaps_ids = [snap['id'] for snap in self.remote_snaps]
291
+ current_snap_ids = [snap['id']
292
+ for snap in self.manifest['service_definitions']]
293
+ for input_snap in input_snaps_list:
294
+ if not input_snap in remote_snaps_ids:
295
+ snapctl_error(
296
+ message="Invalid Snap " + input_snap +
297
+ " provided with --snaps. Please check and try again.",
298
+ code=SNAPCTL_INPUT_ERROR)
299
+ if not input_snap in current_snap_ids:
300
+ snapctl_error(
301
+ message="Snap " + input_snap +
302
+ " provided with --snaps is not present in the manifest. " +
303
+ "Please check and try again.",
304
+ code=SNAPCTL_INPUT_ERROR)
305
+ elif self.subcommand == 'update':
306
+ if not self.manifest_path_filename:
307
+ snapctl_error(
308
+ message="Manifest path is required for update command.",
309
+ code=SNAPCTL_INPUT_ERROR)
310
+ if (not self.add_snaps or self.add_snaps == '') and \
311
+ (not self.remove_snaps or self.remove_snaps == '') and \
312
+ (not self.add_features or self.add_features == '') and \
313
+ (not self.remove_features or self.remove_features == ''):
314
+ snapctl_error(
315
+ message="At least one of --add-snaps, --remove-snaps, add-features " +
316
+ "or --remove-features is required to update a snapend manifest.",
317
+ code=SNAPCTL_INPUT_ERROR)
318
+ if self.add_snaps and self.add_snaps != '':
319
+ input_snaps_list = self.add_snaps.split(',')
320
+ repeat_snaps = check_duplicates_in_list(input_snaps_list)
321
+ if len(repeat_snaps) > 0:
322
+ snapctl_error(
323
+ message="Duplicate snaps found in input: " +
324
+ f"{', '.join(repeat_snaps)}. Please check and try again.",
325
+ code=SNAPCTL_INPUT_ERROR)
326
+ remote_snaps_ids = [snap['id'] for snap in self.remote_snaps]
327
+ for input_snap in input_snaps_list:
328
+ if not input_snap in remote_snaps_ids:
329
+ snapctl_error(
330
+ message="Invalid Snap " + input_snap +
331
+ " provided with --add-snaps. Please check and try again.",
332
+ code=SNAPCTL_INPUT_ERROR)
333
+ if self.remove_snaps and self.remove_snaps != '':
334
+ input_snaps_list = self.remove_snaps.split(',')
335
+ if SnapendManifest.AUTH_SNAP_ID in input_snaps_list:
336
+ snapctl_error(
337
+ message="Auth snap cannot be removed from the manifest.",
338
+ code=SNAPCTL_INPUT_ERROR)
339
+ repeat_snaps = check_duplicates_in_list(input_snaps_list)
340
+ if len(repeat_snaps) > 0:
341
+ snapctl_error(
342
+ message="Duplicate snaps found in input: " +
343
+ f"{', '.join(repeat_snaps)}. Please check and try again.",
344
+ code=SNAPCTL_INPUT_ERROR)
345
+ remote_snaps_ids = [snap['id'] for snap in self.remote_snaps]
346
+ for input_snap in input_snaps_list:
347
+ if not input_snap in remote_snaps_ids:
348
+ snapctl_error(
349
+ message="Invalid Snap " + input_snap +
350
+ " provided with --remove-snaps. Please check and try again.",
351
+ code=SNAPCTL_INPUT_ERROR)
352
+ if self.add_features:
353
+ input_features_list = self.add_features.split(',')
354
+ repeat_features = check_duplicates_in_list(input_features_list)
355
+ if len(repeat_features) > 0:
356
+ snapctl_error(
357
+ message="Duplicate features found in input: " +
358
+ f"{', '.join(repeat_features)}. Please check and try again.",
359
+ code=SNAPCTL_INPUT_ERROR)
360
+ for feature in input_features_list:
361
+ feature = feature.strip()
362
+ if feature.upper() not in SnapendManifest.FEATURES:
363
+ snapctl_error(
364
+ message="--add-features must be one of " +
365
+ f"{', '.join(SnapendManifest.FEATURES)}.",
366
+ code=SNAPCTL_INPUT_ERROR)
367
+ if self.remove_features:
368
+ input_features_list = self.remove_features.split(',')
369
+ repeat_features = check_duplicates_in_list(input_features_list)
370
+ if len(repeat_features) > 0:
371
+ snapctl_error(
372
+ message="Duplicate features found in input: " +
373
+ f"{', '.join(repeat_features)}. Please check and try again.",
374
+ code=SNAPCTL_INPUT_ERROR)
375
+ for feature in input_features_list:
376
+ feature = feature.strip()
377
+ if feature.upper() not in SnapendManifest.FEATURES:
378
+ snapctl_error(
379
+ message="--remove-features must be one of " +
380
+ f"{', '.join(SnapendManifest.FEATURES)}.",
381
+ code=SNAPCTL_INPUT_ERROR)
382
+ if not self.out_path_filename:
383
+ snapctl_error(
384
+ message="Output path is required for update command.",
385
+ code=SNAPCTL_INPUT_ERROR)
386
+ if self.out_path_filename and not (self.out_path_filename.endswith('.json') or
387
+ self.out_path_filename.endswith('.yaml') or
388
+ self.out_path_filename.endswith('.yml')):
389
+ snapctl_error(
390
+ message="Output path must end with .json, .yaml or .yml",
391
+ code=SNAPCTL_INPUT_ERROR)
392
+ if not self.manifest:
393
+ snapctl_error(
394
+ message="Unable to read the manifest file. " +
395
+ "Please check the file and try again.",
396
+ code=SNAPCTL_INPUT_ERROR)
397
+ if 'service_definitions' not in self.manifest:
398
+ snapctl_error(
399
+ message="Invalid manifest file. Need service_definitions. " +
400
+ "Please check the file and try again.",
401
+ code=SNAPCTL_INPUT_ERROR)
402
+ elif self.subcommand == 'validate':
403
+ if not self.manifest_path_filename:
404
+ snapctl_error(
405
+ message="Manifest path is required for validate command.",
406
+ code=SNAPCTL_INPUT_ERROR)
407
+
408
+ def _get_snap_sd(self, snap_id) -> dict:
409
+ """
410
+ Get snap service definition
411
+ """
412
+ for snap in self.remote_snaps:
413
+ if snap['id'] == snap_id:
414
+ snap_sd = {
415
+ "id": snap['id'],
416
+ "language": snap['language'],
417
+ "version": snap['latest_version'],
418
+ "author_id": snap['author_id'],
419
+ "category": snap['category'],
420
+ "subcategory": snap['subcategory'],
421
+ "data_dependencies": [],
422
+ }
423
+ for versions in snap['versions']:
424
+ if versions['version'] == snap['latest_version']:
425
+ snap_sd['data_dependencies'] = \
426
+ versions['data_dependencies']
427
+ return snap_sd
428
+ raise ValueError(
429
+ f"Snap service definition with id '{snap_id}' not found")
430
+
431
+ # Commands
432
+ def create(self) -> bool:
433
+ """
434
+ Create a snapend manifest
435
+ @test -
436
+ `python -m snapctl snapend-manifest create --name my-dev-snapend --env DEVELOPMENT --snaps auth,analytics --out-path-filename ./snapend-create-manifest.json`
437
+ """
438
+ progress = Progress(
439
+ SpinnerColumn(),
440
+ TextColumn("[progress.description]{task.description}"),
441
+ transient=True,
442
+ )
443
+ progress.start()
444
+ progress.add_task(
445
+ description='Enumerating all your games...', total=None)
446
+ try:
447
+ new_manifest = {
448
+ "version": "v1",
449
+ "name": self.name,
450
+ "environment": self.environment,
451
+ "service_definitions": [],
452
+ "feature_definitions": [],
453
+ "external_endpoints": [],
454
+ "settings": []
455
+ }
456
+ if self.snaps and self.snaps != '':
457
+ snap_ids = [snap_id.strip()
458
+ for snap_id in self.snaps.split(',')]
459
+ for snap_id in snap_ids:
460
+ snap_sd = self._get_snap_sd(snap_id)
461
+ new_manifest['service_definitions'].append(snap_sd)
462
+ info(f"Added snap {snap_id} to the manifest.")
463
+ # If auth snap is not present, add it
464
+ found_auth = False
465
+ for final_snap in new_manifest['service_definitions']:
466
+ if final_snap['id'] == SnapendManifest.AUTH_SNAP_ID:
467
+ found_auth = True
468
+ break
469
+ if not found_auth:
470
+ auth_sd = self._get_snap_sd(SnapendManifest.AUTH_SNAP_ID)
471
+ new_manifest['service_definitions'].append(auth_sd)
472
+ warning(
473
+ 'Auth snap is required for snapend. Added auth snap to the manifest.')
474
+ new_manifest['service_definitions'].sort(key=lambda x: x["id"])
475
+ if self.features and self.features != '':
476
+ features = [feature.strip()
477
+ for feature in self.features.split(',')]
478
+ for feature in features:
479
+ if feature.upper() not in new_manifest['feature_definitions']:
480
+ new_manifest['feature_definitions'].append(
481
+ feature.upper())
482
+ info(f"Added feature {feature} to the manifest.")
483
+ if self.out_path_filename:
484
+ # Based on the out-path extension, write JSON or YAML
485
+ if self.out_path_filename.endswith('.yaml') or self.out_path_filename.endswith('.yml'):
486
+ try:
487
+ import yaml # type: ignore
488
+ except ImportError as e:
489
+ snapctl_error(
490
+ message="YAML output requested but PyYAML is not installed. "
491
+ "Install with: pip install pyyaml",
492
+ code=SNAPCTL_INPUT_ERROR,
493
+ progress=progress)
494
+ with open(self.out_path_filename, 'w') as out_file:
495
+ yaml.dump(new_manifest, out_file, sort_keys=False)
496
+ else:
497
+ with open(self.out_path_filename, 'w') as out_file:
498
+ out_file.write(json.dumps(new_manifest, indent=4))
499
+ info(f"Output written to {self.out_path_filename}")
500
+ success("You can now use this manifest to create a snapend " +
501
+ "environment using the command 'snapend create " +
502
+ "--manifest-path-filename $fullPathToManifest --application-id $appId --blocking'")
503
+ snapctl_success(
504
+ message="Snapend manifest created successfully.",
505
+ progress=progress)
506
+ else:
507
+ snapctl_success(
508
+ message=new_manifest, progress=progress)
509
+ except ValueError as e:
510
+ snapctl_error(
511
+ message=f"Exception: {e}",
512
+ code=SNAPCTL_INTERNAL_SERVER_ERROR, progress=progress)
513
+ except RequestException as e:
514
+ snapctl_error(
515
+ message=f"Exception: Unable to create snapend manifest {e}",
516
+ code=SNAPCTL_SNAPEND_MANIFEST_CREATE_ERROR, progress=progress)
517
+ finally:
518
+ progress.stop()
519
+ snapctl_error(
520
+ message='Failed to create snapend manifest.',
521
+ code=SNAPCTL_SNAPEND_MANIFEST_CREATE_ERROR, progress=progress)
522
+
523
+ def sync(self) -> bool:
524
+ """
525
+ Sync with a snapend manifest
526
+ @test -
527
+ `python -m snapctl snapend-manifest sync --manifest-path-filename ./snapend-manifest.json --snaps analytics,auth --features WEB_SOCKETS --out-path-filename ./snapend-updated-manifest.json`
528
+ """
529
+ progress = Progress(
530
+ SpinnerColumn(),
531
+ TextColumn("[progress.description]{task.description}"),
532
+ transient=True,
533
+ )
534
+ progress.start()
535
+ progress.add_task(
536
+ description='Syncing snapend manifest...', total=None)
537
+ try:
538
+ if 'applied_configuration' in self.manifest:
539
+ info('Applied configuration found in the manifest. ')
540
+ warning(
541
+ 'You need to ensure you have synced the manifest from remote. ' +
542
+ 'Else if you try applying the newly generated manifest it may not work.')
543
+ current_snaps = self.manifest['service_definitions']
544
+ current_snap_ids = [snap['id'] for snap in current_snaps]
545
+ final_snaps = []
546
+ if self.snaps and self.snaps != '':
547
+ input_snap_list = self.snaps.split(',')
548
+ for snap_id in input_snap_list:
549
+ snap_id = snap_id.strip()
550
+ if snap_id == '':
551
+ continue
552
+ # Copy existing snap if already present
553
+ if snap_id in current_snap_ids:
554
+ warning(
555
+ f"Snap {snap_id} already exists in the manifest. Skipping...")
556
+ final_snaps.append(
557
+ current_snaps[current_snap_ids.index(snap_id)])
558
+ continue
559
+ # Else add new snap from remote snaps
560
+ snap_sd = self._get_snap_sd(snap_id)
561
+ final_snaps.append(snap_sd)
562
+ info(f"Added snap {snap_id} to the manifest.")
563
+ found_auth = False
564
+ for final_snap in final_snaps:
565
+ if final_snap['id'] == SnapendManifest.AUTH_SNAP_ID:
566
+ found_auth = True
567
+ break
568
+ if not found_auth:
569
+ auth_sd = self._get_snap_sd(SnapendManifest.AUTH_SNAP_ID)
570
+ final_snaps.append(auth_sd)
571
+ warning(
572
+ 'Auth snap is required for snapend. Added auth snap to the manifest.')
573
+ warning(
574
+ f'Old snaps list "{",".join(current_snap_ids)}" will be ' +
575
+ f'replaced with new snaps list "{",".join([snap['id'] for snap in final_snaps])}"')
576
+ final_snaps.sort(key=lambda x: x["id"])
577
+ self.manifest['service_definitions'] = final_snaps
578
+
579
+ final_features = []
580
+ if self.features and self.features != '':
581
+ current_features = self.manifest['feature_definitions']
582
+ input_feature_list = self.features.split(',')
583
+ for feature in input_feature_list:
584
+ feature = feature.strip()
585
+ if feature == '':
586
+ continue
587
+ final_features.append(feature.upper())
588
+ if feature.upper() in current_features:
589
+ warning(
590
+ f"Feature {feature} already exists in the manifest. Skipping...")
591
+ else:
592
+ info(f"Added feature {feature} to the manifest.")
593
+ warning(
594
+ f'Old features list: "{",".join(self.manifest["feature_definitions"])}" will ' +
595
+ f'be replaced with new features list: "{",".join([feature for feature in final_features])}"')
596
+ final_features.sort()
597
+ self.manifest['feature_definitions'] = final_features
598
+
599
+ # Write output
600
+ # Based on the out-path extension, write JSON or YAML
601
+ if self.out_path_filename.endswith('.yaml') or self.out_path_filename.endswith('.yml'):
602
+ try:
603
+ import yaml # type: ignore
604
+ except ImportError as e:
605
+ snapctl_error(
606
+ message="YAML output requested but PyYAML is not installed. "
607
+ "Install with: pip install pyyaml",
608
+ code=SNAPCTL_INPUT_ERROR,
609
+ progress=progress)
610
+ with open(self.out_path_filename, 'w') as out_file:
611
+ yaml.dump(self.manifest, out_file, sort_keys=False)
612
+ else:
613
+ with open(self.out_path_filename, 'w') as out_file:
614
+ out_file.write(json.dumps(self.manifest, indent=4))
615
+ info(f"Output written to {self.out_path_filename}")
616
+ snapctl_success(
617
+ message="Snapend manifest synced successfully.",
618
+ progress=progress)
619
+ except ValueError as e:
620
+ snapctl_error(
621
+ message=f"Exception: {e}",
622
+ code=SNAPCTL_INTERNAL_SERVER_ERROR, progress=progress)
623
+ except RequestException as e:
624
+ snapctl_error(
625
+ message=f"Exception: Unable to synced snapend manifest {e}",
626
+ code=SNAPCTL_SNAPEND_MANIFEST_SYNC_ERROR, progress=progress)
627
+ finally:
628
+ progress.stop()
629
+ snapctl_error(
630
+ message='Failed to synced the snapend manifest.',
631
+ code=SNAPCTL_SNAPEND_MANIFEST_SYNC_ERROR, progress=progress)
632
+
633
+ def upgrade(self) -> bool:
634
+ """
635
+ Upgrade all Snap versions to the latest in a snapend manifest
636
+ @test -
637
+ `python -m snapctl snapend-manifest upgrade --manifest-path-filename ./snapser-upgrade-manifest.json --snaps auth,analytics --out-path-filename ./snapend-upgraded-manifest.json`
638
+ `python -m snapctl snapend-manifest upgrade --manifest-path-filename ./snapser-upgrade-manifest.json --out-path-filename ./snapend-upgraded-manifest.json`
639
+ """
640
+ progress = Progress(
641
+ SpinnerColumn(),
642
+ TextColumn("[progress.description]{task.description}"),
643
+ transient=True,
644
+ )
645
+ progress.start()
646
+ progress.add_task(
647
+ description='Updating snapend manifest...', total=None)
648
+ try:
649
+ if 'applied_configuration' in self.manifest:
650
+ info('Applied configuration found in the manifest. ')
651
+ warning(
652
+ 'You need to ensure you have synced the manifest from remote. ' +
653
+ 'Else if you try applying the newly generated manifest it may not work.')
654
+
655
+ current_snaps = self.manifest['service_definitions']
656
+ force_snaps_upgrade = []
657
+ if self.snaps and self.snaps != '':
658
+ force_snaps_upgrade = [snap_id.strip()
659
+ for snap_id in self.snaps.split(',')]
660
+ # Look at self.remote_snaps, get the latest version for each snap
661
+ for i, snap in enumerate(current_snaps):
662
+ for remote_snap in self.remote_snaps:
663
+ if remote_snap['id'] == snap['id']:
664
+ if len(force_snaps_upgrade) > 0 and \
665
+ snap['id'] not in force_snaps_upgrade:
666
+ info(
667
+ f"Skipping snap {snap['id']} as it's not in the " +
668
+ f"--snaps list {','.join(force_snaps_upgrade)}")
669
+ break
670
+ if remote_snap['latest_version'] != snap['version']:
671
+ current_snaps[i] = self._get_snap_sd(snap['id'])
672
+ info(
673
+ f"Upgraded snap {snap['id']} from version " +
674
+ f"{snap['version']} to {remote_snap['latest_version']}.")
675
+ else:
676
+ info(
677
+ f"Snap {snap['id']} is already at the latest " +
678
+ f"version {snap['version']}. Skipping...")
679
+ break
680
+ current_snaps.sort(key=lambda x: x["id"])
681
+ self.manifest['service_definitions'] = current_snaps
682
+
683
+ # Write output
684
+ # Based on the out-path extension, write JSON or YAML
685
+ if self.out_path_filename.endswith('.yaml') or self.out_path_filename.endswith('.yml'):
686
+ try:
687
+ import yaml # type: ignore
688
+ except ImportError as e:
689
+ snapctl_error(
690
+ message="YAML output requested but PyYAML is not installed. "
691
+ "Install with: pip install pyyaml",
692
+ code=SNAPCTL_INPUT_ERROR,
693
+ progress=progress)
694
+ with open(self.out_path_filename, 'w') as out_file:
695
+ yaml.dump(self.manifest, out_file, sort_keys=False)
696
+ else:
697
+ with open(self.out_path_filename, 'w') as out_file:
698
+ out_file.write(json.dumps(self.manifest, indent=4))
699
+ info(f"Output written to {self.out_path_filename}")
700
+ snapctl_success(
701
+ message="Snapend manifest upgraded successfully.",
702
+ progress=progress)
703
+ except ValueError as e:
704
+ snapctl_error(
705
+ message=f"Exception: {e}",
706
+ code=SNAPCTL_INTERNAL_SERVER_ERROR, progress=progress)
707
+ except RequestException as e:
708
+ snapctl_error(
709
+ message=f"Exception: Unable to upgrade the snapend manifest {e}",
710
+ code=SNAPCTL_SNAPEND_MANIFEST_UPGRADE_ERROR, progress=progress)
711
+ finally:
712
+ progress.stop()
713
+ snapctl_error(
714
+ message='Failed to upgrade the snapend manifest.',
715
+ code=SNAPCTL_SNAPEND_MANIFEST_UPGRADE_ERROR, progress=progress)
716
+
717
+ def update(self) -> bool:
718
+ """
719
+ Update a snapend manifest
720
+ @test -
721
+ `python -m snapctl snapend-manifest update --manifest-path-filename ./snapend-manifest.json --add-snaps analytics,auth --add-features WEB_SOCKETS --out-path-filename ./snapend-updated-manifest.json`
722
+ """
723
+ progress = Progress(
724
+ SpinnerColumn(),
725
+ TextColumn("[progress.description]{task.description}"),
726
+ transient=True,
727
+ )
728
+ progress.start()
729
+ progress.add_task(
730
+ description='Updating snapend manifest...', total=None)
731
+ try:
732
+ if 'applied_configuration' in self.manifest:
733
+ info('Applied configuration found in the manifest. ')
734
+ warning(
735
+ 'You need to ensure you have synced the manifest from remote. ' +
736
+ 'Else if you try applying the newly generated manifest it may not work.')
737
+ current_snaps = self.manifest['service_definitions']
738
+ current_snap_ids = [snap['id'] for snap in current_snaps]
739
+ final_added_snap_ids = []
740
+ final_removed_snap_ids = []
741
+ if self.add_snaps and self.add_snaps != '':
742
+ add_snap_list = self.add_snaps.split(',')
743
+ for snap_id in add_snap_list:
744
+ snap_id = snap_id.strip()
745
+ if snap_id == '':
746
+ continue
747
+ # Copy existing snap if already present
748
+ if snap_id in current_snap_ids:
749
+ warning(
750
+ f"Snap {snap_id} already exists in the manifest. Skipping...")
751
+ continue
752
+ # Else add new snap from remote snaps
753
+ snap_sd = self._get_snap_sd(snap_id)
754
+ current_snaps.append(snap_sd)
755
+ current_snap_ids.append(snap_id)
756
+ final_added_snap_ids.append(snap_id)
757
+ info(f"Added snap {snap_id} to the manifest.")
758
+ if self.remove_snaps and self.remove_snaps != '':
759
+ remove_snap_list = self.remove_snaps.split(',')
760
+ for snap_id in remove_snap_list:
761
+ snap_id = snap_id.strip()
762
+ if snap_id == '':
763
+ continue
764
+ if snap_id not in current_snap_ids:
765
+ warning(
766
+ f"Snap {snap_id} does not exist in the manifest. Skipping...")
767
+ continue
768
+ # Remove snap from current snaps
769
+ index = current_snap_ids.index(snap_id)
770
+ current_snaps.pop(index)
771
+ current_snap_ids.pop(index)
772
+ final_removed_snap_ids.append(snap_id)
773
+ info(f"Removed snap {snap_id} from the manifest.")
774
+
775
+ found_auth = False
776
+ for final_snap in current_snap_ids:
777
+ if final_snap == SnapendManifest.AUTH_SNAP_ID:
778
+ found_auth = True
779
+ break
780
+ if not found_auth:
781
+ auth_sd = self._get_snap_sd(SnapendManifest.AUTH_SNAP_ID)
782
+ current_snaps.append(auth_sd)
783
+ current_snap_ids.append(SnapendManifest.AUTH_SNAP_ID)
784
+ final_added_snap_ids.append(SnapendManifest.AUTH_SNAP_ID)
785
+ warning(
786
+ 'Auth snap is required for snapend. Added auth snap to the manifest.')
787
+ warning(
788
+ f'New snaps "{",".join(final_added_snap_ids)}" were added. ' +
789
+ f'Snaps "{",".join(final_removed_snap_ids)}" were removed. ')
790
+ current_snaps.sort(key=lambda x: x["id"])
791
+ self.manifest['service_definitions'] = current_snaps
792
+
793
+ current_features = self.manifest['feature_definitions']
794
+ final_added_features = []
795
+ final_removed_features = []
796
+ if self.add_features and self.add_features != '':
797
+ add_feature_list = self.add_features.split(',')
798
+ for feature in add_feature_list:
799
+ feature = feature.strip()
800
+ if feature == '':
801
+ continue
802
+ if feature.upper() in current_features:
803
+ warning(
804
+ f"Feature {feature} already exists in the manifest. Skipping...")
805
+ continue
806
+ current_features.append(feature.upper())
807
+ final_added_features.append(feature.upper())
808
+ info(f"Added feature {feature} to the manifest.")
809
+ if self.remove_features and self.remove_features != '':
810
+ remove_feature_list = self.remove_features.split(',')
811
+ for feature in remove_feature_list:
812
+ feature = feature.strip()
813
+ if feature == '':
814
+ continue
815
+ if feature.upper() not in current_features:
816
+ warning(
817
+ f"Feature {feature} does not exist in the manifest. Skipping...")
818
+ continue
819
+ index = current_features.index(feature.upper())
820
+ current_features.pop(index)
821
+ final_removed_features.append(feature.upper())
822
+ info(f"Removed feature {feature} from the manifest.")
823
+ warning(
824
+ f'New features "{",".join(final_added_features)}" were added. ' +
825
+ f'Features "{",".join(final_removed_features)}" were removed. ')
826
+ current_features.sort()
827
+ self.manifest['feature_definitions'] = current_features
828
+
829
+ # Write output
830
+ # Based on the out-path extension, write JSON or YAML
831
+ if self.out_path_filename.endswith('.yaml') or self.out_path_filename.endswith('.yml'):
832
+ try:
833
+ import yaml # type: ignore
834
+ except ImportError as e:
835
+ snapctl_error(
836
+ message="YAML output requested but PyYAML is not installed. "
837
+ "Install with: pip install pyyaml",
838
+ code=SNAPCTL_INPUT_ERROR,
839
+ progress=progress)
840
+ with open(self.out_path_filename, 'w') as out_file:
841
+ yaml.dump(self.manifest, out_file, sort_keys=False)
842
+ else:
843
+ with open(self.out_path_filename, 'w') as out_file:
844
+ out_file.write(json.dumps(self.manifest, indent=4))
845
+ info(f"Output written to {self.out_path_filename}")
846
+ snapctl_success(
847
+ message="Snapend manifest updated successfully.",
848
+ progress=progress)
849
+ except ValueError as e:
850
+ snapctl_error(
851
+ message=f"Exception: {e}",
852
+ code=SNAPCTL_INTERNAL_SERVER_ERROR, progress=progress)
853
+ except RequestException as e:
854
+ snapctl_error(
855
+ message=f"Exception: Unable to update snapend manifest {e}",
856
+ code=SNAPCTL_SNAPEND_MANIFEST_UPDATE_ERROR, progress=progress)
857
+ finally:
858
+ progress.stop()
859
+ snapctl_error(
860
+ message='Failed to update the snapend manifest.',
861
+ code=SNAPCTL_SNAPEND_MANIFEST_UPDATE_ERROR, progress=progress)