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.
- snapctl/commands/application.py +135 -0
- snapctl/commands/byows.py +6 -4
- 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.0.3.mdx +1 -2
- snapctl/data/releases/1.0.4.mdx +5 -0
- 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.3.dist-info → snapctl-1.1.0.dist-info}/METADATA +158 -35
- {snapctl-1.0.3.dist-info → snapctl-1.1.0.dist-info}/RECORD +18 -12
- {snapctl-1.0.3.dist-info → snapctl-1.1.0.dist-info}/LICENSE +0 -0
- {snapctl-1.0.3.dist-info → snapctl-1.1.0.dist-info}/WHEEL +0 -0
- {snapctl-1.0.3.dist-info → snapctl-1.1.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,541 @@
|
|
|
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
|
|
12
|
+
from snapctl.commands.snaps import Snaps
|
|
13
|
+
from snapctl.utils.helper import snapctl_error, snapctl_success
|
|
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', '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
|
+
out_path_filename: Union[str, None] = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
self.subcommand: str = subcommand
|
|
36
|
+
self.base_url: str = base_url
|
|
37
|
+
self.api_key: Union[str, None] = api_key
|
|
38
|
+
self.name: str = name
|
|
39
|
+
self.environment: str = environment
|
|
40
|
+
self.manifest_path_filename: Union[str, None] = manifest_path_filename
|
|
41
|
+
self.manifest: Union[dict, None] = None
|
|
42
|
+
self.out_path_filename: Union[str, None] = out_path_filename
|
|
43
|
+
self.snaps = snaps
|
|
44
|
+
self.features = features
|
|
45
|
+
self.remote_snaps: list = self.load_snaps()
|
|
46
|
+
# Setup
|
|
47
|
+
self.setup_manifest()
|
|
48
|
+
# Validate input
|
|
49
|
+
self.validate_input()
|
|
50
|
+
|
|
51
|
+
def setup_manifest(self) -> bool:
|
|
52
|
+
"""
|
|
53
|
+
Read a manifest (JSON or YAML) and saves it
|
|
54
|
+
Supports extensions: .json, .yaml, .yml
|
|
55
|
+
If the extension is unknown, tries JSON then YAML.
|
|
56
|
+
"""
|
|
57
|
+
def parse_json(s: str):
|
|
58
|
+
return json.loads(s)
|
|
59
|
+
|
|
60
|
+
def parse_yaml(s: str):
|
|
61
|
+
try:
|
|
62
|
+
import yaml # type: ignore
|
|
63
|
+
except ImportError as e:
|
|
64
|
+
raise RuntimeError(
|
|
65
|
+
"YAML file provided but PyYAML is not installed. "
|
|
66
|
+
"Install with: pip install pyyaml"
|
|
67
|
+
) from e
|
|
68
|
+
return yaml.safe_load(s)
|
|
69
|
+
|
|
70
|
+
if not self.manifest_path_filename:
|
|
71
|
+
return False
|
|
72
|
+
with open(self.manifest_path_filename, "r", encoding="utf-8") as f:
|
|
73
|
+
text = f.read()
|
|
74
|
+
|
|
75
|
+
ext = os.path.splitext(self.manifest_path_filename)[1].lower()
|
|
76
|
+
if ext == ".json":
|
|
77
|
+
parsers = (parse_json, parse_yaml)
|
|
78
|
+
elif ext in (".yaml", ".yml"):
|
|
79
|
+
parsers = (parse_yaml, parse_json)
|
|
80
|
+
else:
|
|
81
|
+
parsers = (parse_json, parse_yaml)
|
|
82
|
+
|
|
83
|
+
last_err = None
|
|
84
|
+
data = None
|
|
85
|
+
for parser in parsers:
|
|
86
|
+
try:
|
|
87
|
+
data = parser(text)
|
|
88
|
+
break
|
|
89
|
+
except Exception as e:
|
|
90
|
+
last_err = e
|
|
91
|
+
|
|
92
|
+
if data is None:
|
|
93
|
+
return False
|
|
94
|
+
if not isinstance(data, dict):
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
self.manifest = data
|
|
99
|
+
except KeyError as e:
|
|
100
|
+
pass
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
def load_snaps(self) -> list:
|
|
104
|
+
"""
|
|
105
|
+
Load snaps from the Snapser portal
|
|
106
|
+
"""
|
|
107
|
+
snaps_response = Snaps.get_snaps(self.base_url, self.api_key)
|
|
108
|
+
if 'services' in snaps_response:
|
|
109
|
+
return snaps_response['services']
|
|
110
|
+
return []
|
|
111
|
+
|
|
112
|
+
def validate_input(self) -> None:
|
|
113
|
+
"""
|
|
114
|
+
Validator
|
|
115
|
+
"""
|
|
116
|
+
# Check API Key and Base URL
|
|
117
|
+
if not self.api_key or self.base_url == '':
|
|
118
|
+
snapctl_error(
|
|
119
|
+
message="Missing API Key.", code=SNAPCTL_INPUT_ERROR)
|
|
120
|
+
# Check subcommand
|
|
121
|
+
if not self.subcommand in SnapendManifest.SUBCOMMANDS:
|
|
122
|
+
snapctl_error(
|
|
123
|
+
message="Invalid command. Valid commands are " +
|
|
124
|
+
f"{', '.join(SnapendManifest.SUBCOMMANDS)}.",
|
|
125
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
126
|
+
if len(self.remote_snaps) == 0:
|
|
127
|
+
snapctl_error(
|
|
128
|
+
message="Something went wrong. No snaps found. Please try again in some time.",
|
|
129
|
+
code=SNAPCTL_INTERNAL_SERVER_ERROR)
|
|
130
|
+
if self.subcommand == 'create':
|
|
131
|
+
if not self.name or not self.environment:
|
|
132
|
+
snapctl_error(
|
|
133
|
+
message="Name and environment are required for create command.",
|
|
134
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
135
|
+
if self.environment not in SnapendManifest.ENVIRONMENTS:
|
|
136
|
+
snapctl_error(
|
|
137
|
+
message="Environment must be one of " +
|
|
138
|
+
f"{', '.join(SnapendManifest.ENVIRONMENTS)}.",
|
|
139
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
140
|
+
if not self.out_path_filename:
|
|
141
|
+
snapctl_error(
|
|
142
|
+
message="Output path is required for create command.",
|
|
143
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
144
|
+
if self.out_path_filename and not (self.out_path_filename.endswith('.json') or
|
|
145
|
+
self.out_path_filename.endswith('.yaml') or
|
|
146
|
+
self.out_path_filename.endswith('.yml')):
|
|
147
|
+
snapctl_error(
|
|
148
|
+
message="Output path must end with .json, .yaml or .yml",
|
|
149
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
150
|
+
if not self.snaps or self.snaps == '':
|
|
151
|
+
snapctl_error(
|
|
152
|
+
message="At least one snap ID is required to create a " +
|
|
153
|
+
"snapend manifest.",
|
|
154
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
155
|
+
if self.features:
|
|
156
|
+
for feature in self.features.split(','):
|
|
157
|
+
feature = feature.strip()
|
|
158
|
+
if feature.upper() not in SnapendManifest.FEATURES:
|
|
159
|
+
snapctl_error(
|
|
160
|
+
message="-add-features must be one of " +
|
|
161
|
+
f"{', '.join(SnapendManifest.FEATURES)}.",
|
|
162
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
163
|
+
elif self.subcommand == 'update':
|
|
164
|
+
if not self.manifest_path_filename:
|
|
165
|
+
snapctl_error(
|
|
166
|
+
message="Manifest path is required for update command.",
|
|
167
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
168
|
+
if (not self.snaps or self.snaps == '') and \
|
|
169
|
+
(not self.features or self.features == ''):
|
|
170
|
+
snapctl_error(
|
|
171
|
+
message="At least one of snaps or features " +
|
|
172
|
+
"is required to update a snapend manifest.",
|
|
173
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
174
|
+
if not self.out_path_filename:
|
|
175
|
+
snapctl_error(
|
|
176
|
+
message="Output path is required for update command.",
|
|
177
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
178
|
+
if self.out_path_filename and not (self.out_path_filename.endswith('.json') or
|
|
179
|
+
self.out_path_filename.endswith('.yaml') or
|
|
180
|
+
self.out_path_filename.endswith('.yml')):
|
|
181
|
+
snapctl_error(
|
|
182
|
+
message="Output path must end with .json, .yaml or .yml",
|
|
183
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
184
|
+
if not self.manifest:
|
|
185
|
+
snapctl_error(
|
|
186
|
+
message="Unable to read the manifest file. " +
|
|
187
|
+
"Please check the file and try again.",
|
|
188
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
189
|
+
if 'service_definitions' not in self.manifest:
|
|
190
|
+
snapctl_error(
|
|
191
|
+
message="Invalid manifest file. Need service_definitions. " +
|
|
192
|
+
"Please check the file and try again.",
|
|
193
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
194
|
+
elif self.subcommand == 'upgrade':
|
|
195
|
+
if not self.manifest_path_filename:
|
|
196
|
+
snapctl_error(
|
|
197
|
+
message="Manifest path is required for update command.",
|
|
198
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
199
|
+
if not self.out_path_filename:
|
|
200
|
+
snapctl_error(
|
|
201
|
+
message="Output path is required for update command.",
|
|
202
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
203
|
+
if self.out_path_filename and not (self.out_path_filename.endswith('.json') or
|
|
204
|
+
self.out_path_filename.endswith('.yaml') or
|
|
205
|
+
self.out_path_filename.endswith('.yml')):
|
|
206
|
+
snapctl_error(
|
|
207
|
+
message="Output path must end with .json, .yaml or .yml",
|
|
208
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
209
|
+
if not self.manifest:
|
|
210
|
+
snapctl_error(
|
|
211
|
+
message="Unable to read the manifest file. " +
|
|
212
|
+
"Please check the file and try again.",
|
|
213
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
214
|
+
if 'service_definitions' not in self.manifest:
|
|
215
|
+
snapctl_error(
|
|
216
|
+
message="Invalid manifest file. Need service_definitions. " +
|
|
217
|
+
"Please check the file and try again.",
|
|
218
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
219
|
+
elif self.subcommand == 'validate':
|
|
220
|
+
if not self.manifest_path_filename:
|
|
221
|
+
snapctl_error(
|
|
222
|
+
message="Manifest path is required for validate command.",
|
|
223
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
224
|
+
|
|
225
|
+
def _get_snap_sd(self, snap_id) -> dict:
|
|
226
|
+
"""
|
|
227
|
+
Get snap service definition
|
|
228
|
+
"""
|
|
229
|
+
for snap in self.remote_snaps:
|
|
230
|
+
if snap['id'] == snap_id:
|
|
231
|
+
snap_sd = {
|
|
232
|
+
"id": snap['id'],
|
|
233
|
+
"language": snap['language'],
|
|
234
|
+
"version": snap['latest_version'],
|
|
235
|
+
"author_id": snap['author_id'],
|
|
236
|
+
"category": snap['category'],
|
|
237
|
+
"subcategory": snap['subcategory'],
|
|
238
|
+
"data_dependencies": [],
|
|
239
|
+
}
|
|
240
|
+
for versions in snap['versions']:
|
|
241
|
+
if versions['version'] == snap['latest_version']:
|
|
242
|
+
snap_sd['data_dependencies'] = \
|
|
243
|
+
versions['data_dependencies']
|
|
244
|
+
return snap_sd
|
|
245
|
+
raise ValueError(
|
|
246
|
+
f"Snap service definition with id '{snap_id}' not found")
|
|
247
|
+
|
|
248
|
+
# Commands
|
|
249
|
+
def create(self) -> bool:
|
|
250
|
+
"""
|
|
251
|
+
Create a snapend manifest
|
|
252
|
+
@test -
|
|
253
|
+
`python -m snapctl snapend-manifest create --name my-dev-snapend --env DEVELOPMENT --snaps auth,analytics --out-path-filename ./snapend-manifest.json`
|
|
254
|
+
"""
|
|
255
|
+
progress = Progress(
|
|
256
|
+
SpinnerColumn(),
|
|
257
|
+
TextColumn("[progress.description]{task.description}"),
|
|
258
|
+
transient=True,
|
|
259
|
+
)
|
|
260
|
+
progress.start()
|
|
261
|
+
progress.add_task(
|
|
262
|
+
description='Enumerating all your games...', total=None)
|
|
263
|
+
try:
|
|
264
|
+
new_manifest = {
|
|
265
|
+
"version": "v1",
|
|
266
|
+
"name": self.name,
|
|
267
|
+
"environment": self.environment,
|
|
268
|
+
"service_definitions": [],
|
|
269
|
+
"feature_definitions": [],
|
|
270
|
+
"external_endpoints": [],
|
|
271
|
+
"settings": []
|
|
272
|
+
}
|
|
273
|
+
snap_ids = [snap_id.strip()
|
|
274
|
+
for snap_id in self.snaps.split(',')]
|
|
275
|
+
for snap_id in snap_ids:
|
|
276
|
+
snap_found = False
|
|
277
|
+
for snap in self.remote_snaps:
|
|
278
|
+
if snap['id'] == snap_id:
|
|
279
|
+
snap_sd = self._get_snap_sd(snap_id)
|
|
280
|
+
new_manifest['service_definitions'].append(snap_sd)
|
|
281
|
+
snap_found = True
|
|
282
|
+
break
|
|
283
|
+
if not snap_found:
|
|
284
|
+
snapctl_error(
|
|
285
|
+
message=f"Snap ID {snap_id} not found in your snaps.",
|
|
286
|
+
code=SNAPCTL_INPUT_ERROR,
|
|
287
|
+
progress=progress)
|
|
288
|
+
found_auth = False
|
|
289
|
+
for final_snap in new_manifest['service_definitions']:
|
|
290
|
+
if final_snap['id'] == SnapendManifest.AUTH_SNAP_ID:
|
|
291
|
+
found_auth = True
|
|
292
|
+
break
|
|
293
|
+
if not found_auth:
|
|
294
|
+
auth_sd = self._get_snap_sd(SnapendManifest.AUTH_SNAP_ID)
|
|
295
|
+
new_manifest['service_definitions'].append(auth_sd)
|
|
296
|
+
warning(
|
|
297
|
+
'Auth snap is required for snapend. Added auth snap to the manifest.')
|
|
298
|
+
new_manifest['service_definitions'].sort(key=lambda x: x["id"])
|
|
299
|
+
if self.features and self.features != '':
|
|
300
|
+
features = [feature.strip()
|
|
301
|
+
for feature in self.features.split(',')]
|
|
302
|
+
for feature in features:
|
|
303
|
+
if feature.upper() not in new_manifest['feature_definitions']:
|
|
304
|
+
new_manifest['feature_definitions'].append(
|
|
305
|
+
feature.upper())
|
|
306
|
+
if self.out_path_filename:
|
|
307
|
+
# Based on the out-path extension, write JSON or YAML
|
|
308
|
+
if self.out_path_filename.endswith('.yaml') or self.out_path_filename.endswith('.yml'):
|
|
309
|
+
try:
|
|
310
|
+
import yaml # type: ignore
|
|
311
|
+
except ImportError as e:
|
|
312
|
+
snapctl_error(
|
|
313
|
+
message="YAML output requested but PyYAML is not installed. "
|
|
314
|
+
"Install with: pip install pyyaml",
|
|
315
|
+
code=SNAPCTL_INPUT_ERROR,
|
|
316
|
+
progress=progress)
|
|
317
|
+
with open(self.out_path_filename, 'w') as out_file:
|
|
318
|
+
yaml.dump(new_manifest, out_file, sort_keys=False)
|
|
319
|
+
else:
|
|
320
|
+
with open(self.out_path_filename, 'w') as out_file:
|
|
321
|
+
out_file.write(json.dumps(new_manifest, indent=4))
|
|
322
|
+
info(f"Output written to {self.out_path_filename}")
|
|
323
|
+
success("You can now use this manifest to create a snapend " +
|
|
324
|
+
"environment using the command 'snapend create " +
|
|
325
|
+
"--manifest-path-filename $fullPathToManifest --application-id $appId --blocking'")
|
|
326
|
+
snapctl_success(
|
|
327
|
+
message="Snapend manifest created successfully.",
|
|
328
|
+
progress=progress)
|
|
329
|
+
else:
|
|
330
|
+
snapctl_success(
|
|
331
|
+
message=new_manifest, progress=progress)
|
|
332
|
+
except ValueError as e:
|
|
333
|
+
snapctl_error(
|
|
334
|
+
message=f"Exception: {e}",
|
|
335
|
+
code=SNAPCTL_INTERNAL_SERVER_ERROR, progress=progress)
|
|
336
|
+
except RequestException as e:
|
|
337
|
+
snapctl_error(
|
|
338
|
+
message=f"Exception: Unable to create snapend manifest {e}",
|
|
339
|
+
code=SNAPCTL_SNAPEND_MANIFEST_CREATE_ERROR, progress=progress)
|
|
340
|
+
finally:
|
|
341
|
+
progress.stop()
|
|
342
|
+
snapctl_error(
|
|
343
|
+
message='Failed to create snapend manifest.',
|
|
344
|
+
code=SNAPCTL_SNAPEND_MANIFEST_CREATE_ERROR, progress=progress)
|
|
345
|
+
|
|
346
|
+
def update(self) -> bool:
|
|
347
|
+
"""
|
|
348
|
+
Update a snapend manifest
|
|
349
|
+
@test -
|
|
350
|
+
`python -m snapctl snapend-manifest update --manifest-path-filename ./snapend-manifest.json --features WEB_SOCKETS --out-path-filename ./snapend-updated-manifest.json`
|
|
351
|
+
"""
|
|
352
|
+
progress = Progress(
|
|
353
|
+
SpinnerColumn(),
|
|
354
|
+
TextColumn("[progress.description]{task.description}"),
|
|
355
|
+
transient=True,
|
|
356
|
+
)
|
|
357
|
+
progress.start()
|
|
358
|
+
progress.add_task(
|
|
359
|
+
description='Updating snapend manifest...', total=None)
|
|
360
|
+
try:
|
|
361
|
+
if 'applied_configuration' in self.manifest:
|
|
362
|
+
info('Applied configuration found in the manifest. ')
|
|
363
|
+
warning(
|
|
364
|
+
'You need to ensure you have synced the manifest from remote. ' +
|
|
365
|
+
'Else if you try applying the newly generated manifest it may not work.')
|
|
366
|
+
|
|
367
|
+
current_snaps = self.manifest['service_definitions']
|
|
368
|
+
current_snap_ids = [snap['id'] for snap in current_snaps]
|
|
369
|
+
final_snaps = []
|
|
370
|
+
if self.snaps and self.snaps != '':
|
|
371
|
+
for snap_id in self.snaps.split(','):
|
|
372
|
+
snap_id = snap_id.strip()
|
|
373
|
+
if snap_id == '':
|
|
374
|
+
continue
|
|
375
|
+
if snap_id in current_snap_ids:
|
|
376
|
+
warning(
|
|
377
|
+
f"Snap {snap_id} already exists in the manifest. Skipping...")
|
|
378
|
+
final_snaps.append(
|
|
379
|
+
current_snaps[current_snap_ids.index(snap_id)])
|
|
380
|
+
continue
|
|
381
|
+
snap_found = False
|
|
382
|
+
for snap in self.remote_snaps:
|
|
383
|
+
if snap['id'] == snap_id:
|
|
384
|
+
snap_found = True
|
|
385
|
+
snap_sd = self._get_snap_sd(snap_id)
|
|
386
|
+
final_snaps.append(snap_sd)
|
|
387
|
+
info(f"Added snap {snap_id} to the manifest.")
|
|
388
|
+
break
|
|
389
|
+
if not snap_found:
|
|
390
|
+
snapctl_error(
|
|
391
|
+
message=f"Snap ID {snap_id} not found in your snaps.",
|
|
392
|
+
code=SNAPCTL_INPUT_ERROR,
|
|
393
|
+
progress=progress)
|
|
394
|
+
found_auth = False
|
|
395
|
+
for final_snap in final_snaps:
|
|
396
|
+
if final_snap['id'] == SnapendManifest.AUTH_SNAP_ID:
|
|
397
|
+
found_auth = True
|
|
398
|
+
break
|
|
399
|
+
if not found_auth:
|
|
400
|
+
auth_sd = self._get_snap_sd(SnapendManifest.AUTH_SNAP_ID)
|
|
401
|
+
final_snaps.append(auth_sd)
|
|
402
|
+
warning(
|
|
403
|
+
'Auth snap is required for snapend. Added auth snap to the manifest.')
|
|
404
|
+
warning(
|
|
405
|
+
f'Old snaps list "{",".join(current_snap_ids)}" will be ' +
|
|
406
|
+
f'replaced with new snaps list "{",".join([snap['id'] for snap in final_snaps])}"')
|
|
407
|
+
self.manifest['service_definitions'] = final_snaps
|
|
408
|
+
|
|
409
|
+
final_features = []
|
|
410
|
+
if self.features and self.features != '':
|
|
411
|
+
current_features = self.manifest['feature_definitions']
|
|
412
|
+
for feature in self.features.split(','):
|
|
413
|
+
feature = feature.strip()
|
|
414
|
+
if feature == '':
|
|
415
|
+
continue
|
|
416
|
+
if feature.upper() in current_features:
|
|
417
|
+
warning(
|
|
418
|
+
f"Feature {feature} already exists in the manifest. Skipping...")
|
|
419
|
+
final_features.append(feature.upper())
|
|
420
|
+
warning(
|
|
421
|
+
f'Old features list: "{",".join(self.manifest["feature_definitions"])}" will ' +
|
|
422
|
+
f'be replaced with new features list: "{",".join([feature for feature in final_features])}"')
|
|
423
|
+
final_features.sort()
|
|
424
|
+
self.manifest['feature_definitions'] = final_features
|
|
425
|
+
|
|
426
|
+
# Write output
|
|
427
|
+
# Based on the out-path extension, write JSON or YAML
|
|
428
|
+
if self.out_path_filename.endswith('.yaml') or self.out_path_filename.endswith('.yml'):
|
|
429
|
+
try:
|
|
430
|
+
import yaml # type: ignore
|
|
431
|
+
except ImportError as e:
|
|
432
|
+
snapctl_error(
|
|
433
|
+
message="YAML output requested but PyYAML is not installed. "
|
|
434
|
+
"Install with: pip install pyyaml",
|
|
435
|
+
code=SNAPCTL_INPUT_ERROR,
|
|
436
|
+
progress=progress)
|
|
437
|
+
with open(self.out_path_filename, 'w') as out_file:
|
|
438
|
+
yaml.dump(self.manifest, out_file, sort_keys=False)
|
|
439
|
+
else:
|
|
440
|
+
with open(self.out_path_filename, 'w') as out_file:
|
|
441
|
+
out_file.write(json.dumps(self.manifest, indent=4))
|
|
442
|
+
info(f"Output written to {self.out_path_filename}")
|
|
443
|
+
snapctl_success(
|
|
444
|
+
message="Snapend manifest updated successfully.",
|
|
445
|
+
progress=progress)
|
|
446
|
+
except ValueError as e:
|
|
447
|
+
snapctl_error(
|
|
448
|
+
message=f"Exception: {e}",
|
|
449
|
+
code=SNAPCTL_INTERNAL_SERVER_ERROR, progress=progress)
|
|
450
|
+
except RequestException as e:
|
|
451
|
+
snapctl_error(
|
|
452
|
+
message=f"Exception: Unable to update snapend manifest {e}",
|
|
453
|
+
code=SNAPCTL_SNAPEND_MANIFEST_UPDATE_ERROR, progress=progress)
|
|
454
|
+
finally:
|
|
455
|
+
progress.stop()
|
|
456
|
+
snapctl_error(
|
|
457
|
+
message='Failed to update the snapend manifest.',
|
|
458
|
+
code=SNAPCTL_SNAPEND_MANIFEST_UPDATE_ERROR, progress=progress)
|
|
459
|
+
|
|
460
|
+
def upgrade(self) -> bool:
|
|
461
|
+
"""
|
|
462
|
+
Upgrade all Snap versions to the latest in a snapend manifest
|
|
463
|
+
@test -
|
|
464
|
+
`python -m snapctl snapend-manifest upgrade --manifest-path-filename ./snapser-upgrade-manifest.json --snaps auth,analytics --out-path-filename ./snapend-upgraded-manifest.json`
|
|
465
|
+
`python -m snapctl snapend-manifest upgrade --manifest-path-filename ./snapser-upgrade-manifest.json --out-path-filename ./snapend-upgraded-manifest.json`
|
|
466
|
+
"""
|
|
467
|
+
progress = Progress(
|
|
468
|
+
SpinnerColumn(),
|
|
469
|
+
TextColumn("[progress.description]{task.description}"),
|
|
470
|
+
transient=True,
|
|
471
|
+
)
|
|
472
|
+
progress.start()
|
|
473
|
+
progress.add_task(
|
|
474
|
+
description='Updating snapend manifest...', total=None)
|
|
475
|
+
try:
|
|
476
|
+
if 'applied_configuration' in self.manifest:
|
|
477
|
+
info('Applied configuration found in the manifest. ')
|
|
478
|
+
warning(
|
|
479
|
+
'You need to ensure you have synced the manifest from remote. ' +
|
|
480
|
+
'Else if you try applying the newly generated manifest it may not work.')
|
|
481
|
+
|
|
482
|
+
current_snaps = self.manifest['service_definitions']
|
|
483
|
+
force_snaps_upgrade = []
|
|
484
|
+
if self.snaps and self.snaps != '':
|
|
485
|
+
force_snaps_upgrade = [snap_id.strip()
|
|
486
|
+
for snap_id in self.snaps.split(',')]
|
|
487
|
+
# Look at self.remote_snaps, get the latest version for each snap
|
|
488
|
+
for i, snap in enumerate(current_snaps):
|
|
489
|
+
for remote_snap in self.remote_snaps:
|
|
490
|
+
if remote_snap['id'] == snap['id']:
|
|
491
|
+
if len(force_snaps_upgrade) > 0 and \
|
|
492
|
+
snap['id'] not in force_snaps_upgrade:
|
|
493
|
+
info(
|
|
494
|
+
f"Skipping snap {snap['id']} as it's not in the " +
|
|
495
|
+
f"--snaps list {','.join(force_snaps_upgrade)}")
|
|
496
|
+
break
|
|
497
|
+
if remote_snap['latest_version'] != snap['version']:
|
|
498
|
+
current_snaps[i] = self._get_snap_sd(snap['id'])
|
|
499
|
+
info(
|
|
500
|
+
f"Upgraded snap {snap['id']} from version " +
|
|
501
|
+
f"{snap['version']} to {remote_snap['latest_version']}.")
|
|
502
|
+
else:
|
|
503
|
+
info(
|
|
504
|
+
f"Snap {snap['id']} is already at the latest " +
|
|
505
|
+
f"version {snap['version']}. Skipping...")
|
|
506
|
+
break
|
|
507
|
+
self.manifest['service_definitions'] = current_snaps
|
|
508
|
+
|
|
509
|
+
# Write output
|
|
510
|
+
# Based on the out-path extension, write JSON or YAML
|
|
511
|
+
if self.out_path_filename.endswith('.yaml') or self.out_path_filename.endswith('.yml'):
|
|
512
|
+
try:
|
|
513
|
+
import yaml # type: ignore
|
|
514
|
+
except ImportError as e:
|
|
515
|
+
snapctl_error(
|
|
516
|
+
message="YAML output requested but PyYAML is not installed. "
|
|
517
|
+
"Install with: pip install pyyaml",
|
|
518
|
+
code=SNAPCTL_INPUT_ERROR,
|
|
519
|
+
progress=progress)
|
|
520
|
+
with open(self.out_path_filename, 'w') as out_file:
|
|
521
|
+
yaml.dump(self.manifest, out_file, sort_keys=False)
|
|
522
|
+
else:
|
|
523
|
+
with open(self.out_path_filename, 'w') as out_file:
|
|
524
|
+
out_file.write(json.dumps(self.manifest, indent=4))
|
|
525
|
+
info(f"Output written to {self.out_path_filename}")
|
|
526
|
+
snapctl_success(
|
|
527
|
+
message="Snapend manifest upgraded successfully.",
|
|
528
|
+
progress=progress)
|
|
529
|
+
except ValueError as e:
|
|
530
|
+
snapctl_error(
|
|
531
|
+
message=f"Exception: {e}",
|
|
532
|
+
code=SNAPCTL_INTERNAL_SERVER_ERROR, progress=progress)
|
|
533
|
+
except RequestException as e:
|
|
534
|
+
snapctl_error(
|
|
535
|
+
message=f"Exception: Unable to upgrade the snapend manifest {e}",
|
|
536
|
+
code=SNAPCTL_SNAPEND_MANIFEST_UPGRADE_ERROR, progress=progress)
|
|
537
|
+
finally:
|
|
538
|
+
progress.stop()
|
|
539
|
+
snapctl_error(
|
|
540
|
+
message='Failed to upgrade the snapend manifest.',
|
|
541
|
+
code=SNAPCTL_SNAPEND_MANIFEST_UPGRADE_ERROR, progress=progress)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Snaps CLI commands
|
|
3
|
+
"""
|
|
4
|
+
import json
|
|
5
|
+
from typing import Union
|
|
6
|
+
import requests
|
|
7
|
+
from requests.exceptions import RequestException
|
|
8
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
9
|
+
from snapctl.config.constants import SERVER_CALL_TIMEOUT, SNAPCTL_INPUT_ERROR, \
|
|
10
|
+
SNAPCTL_SNAPS_ENUMERATE_ERROR, SNAPCTL_INTERNAL_SERVER_ERROR
|
|
11
|
+
from snapctl.utils.helper import snapctl_error, snapctl_success
|
|
12
|
+
from snapctl.utils.echo import info
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Snaps:
|
|
16
|
+
"""
|
|
17
|
+
CLI commands exposed for Snaps
|
|
18
|
+
"""
|
|
19
|
+
SUBCOMMANDS = ['enumerate']
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self, *, subcommand: str, base_url: str, api_key: Union[str, None],
|
|
23
|
+
out_path_filename: 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.out_path_filename: Union[str, None] = out_path_filename
|
|
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 Snaps.SUBCOMMANDS:
|
|
42
|
+
snapctl_error(
|
|
43
|
+
message="Invalid command. Valid commands are " +
|
|
44
|
+
f"{', '.join(Snaps.SUBCOMMANDS)}.",
|
|
45
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
46
|
+
if self.subcommand == 'enumerate':
|
|
47
|
+
if self.out_path_filename:
|
|
48
|
+
if not (self.out_path_filename.endswith('.json')):
|
|
49
|
+
snapctl_error(
|
|
50
|
+
message="Output filename should end with .json",
|
|
51
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
52
|
+
info(f"Output will be written to {self.out_path_filename}")
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def get_snaps(base_url: str, api_key: str) -> dict:
|
|
56
|
+
"""
|
|
57
|
+
Get snaps
|
|
58
|
+
"""
|
|
59
|
+
response_json = {}
|
|
60
|
+
try:
|
|
61
|
+
url = f"{base_url}/v1/snapser-api/services"
|
|
62
|
+
res = requests.get(
|
|
63
|
+
url, headers={'api-key': api_key},
|
|
64
|
+
timeout=SERVER_CALL_TIMEOUT
|
|
65
|
+
)
|
|
66
|
+
response_json = res.json()
|
|
67
|
+
except RequestException as e:
|
|
68
|
+
pass
|
|
69
|
+
return response_json
|
|
70
|
+
|
|
71
|
+
def enumerate(self) -> bool:
|
|
72
|
+
"""
|
|
73
|
+
Enumerate all snaps
|
|
74
|
+
"""
|
|
75
|
+
progress = Progress(
|
|
76
|
+
SpinnerColumn(),
|
|
77
|
+
TextColumn("[progress.description]{task.description}"),
|
|
78
|
+
transient=True,
|
|
79
|
+
)
|
|
80
|
+
progress.start()
|
|
81
|
+
progress.add_task(
|
|
82
|
+
description='Enumerating snaps...', total=None)
|
|
83
|
+
try:
|
|
84
|
+
response_json = Snaps.get_snaps(self.base_url, self.api_key)
|
|
85
|
+
if response_json == {}:
|
|
86
|
+
snapctl_error(
|
|
87
|
+
message="Something went wrong. No snaps found. Please try again in some time.",
|
|
88
|
+
code=SNAPCTL_INTERNAL_SERVER_ERROR, progress=progress)
|
|
89
|
+
if 'services' not in response_json:
|
|
90
|
+
snapctl_error(
|
|
91
|
+
message="Something went wrong. No snaps found. Please try again in some time.",
|
|
92
|
+
code=SNAPCTL_SNAPS_ENUMERATE_ERROR, progress=progress)
|
|
93
|
+
if self.out_path_filename:
|
|
94
|
+
with open(self.out_path_filename, 'w') as out_file:
|
|
95
|
+
out_file.write(json.dumps(response_json))
|
|
96
|
+
snapctl_success(
|
|
97
|
+
message=f"Output written to {self.out_path_filename}", progress=progress)
|
|
98
|
+
else:
|
|
99
|
+
snapctl_success(
|
|
100
|
+
message=response_json, progress=progress)
|
|
101
|
+
except RequestException as e:
|
|
102
|
+
snapctl_error(
|
|
103
|
+
message=f"Exception: Unable to enumerate snaps {e}",
|
|
104
|
+
code=SNAPCTL_SNAPS_ENUMERATE_ERROR, progress=progress)
|
|
105
|
+
finally:
|
|
106
|
+
progress.stop()
|
|
107
|
+
snapctl_error(
|
|
108
|
+
message='Failed to enumerate snaps.',
|
|
109
|
+
code=SNAPCTL_SNAPS_ENUMERATE_ERROR, progress=progress)
|