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.
- snapctl/commands/application.py +135 -0
- snapctl/commands/game.py +3 -3
- snapctl/commands/snapend.py +246 -28
- snapctl/commands/snapend_manifest.py +861 -0
- snapctl/commands/snaps.py +109 -0
- snapctl/config/constants.py +19 -2
- snapctl/data/releases/1.1.0.mdx +20 -0
- snapctl/data/releases/1.1.1.mdx +8 -0
- snapctl/main.py +181 -10
- snapctl/utils/exceptions.py +8 -0
- snapctl/utils/helper.py +10 -1
- {snapctl-1.0.4.dist-info → snapctl-1.1.1.dist-info}/METADATA +192 -35
- {snapctl-1.0.4.dist-info → snapctl-1.1.1.dist-info}/RECORD +16 -10
- {snapctl-1.0.4.dist-info → snapctl-1.1.1.dist-info}/LICENSE +0 -0
- {snapctl-1.0.4.dist-info → snapctl-1.1.1.dist-info}/WHEEL +0 -0
- {snapctl-1.0.4.dist-info → snapctl-1.1.1.dist-info}/entry_points.txt +0 -0
|
@@ -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)
|