snapctl 0.44.1__py3-none-any.whl → 0.46.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.

@@ -13,6 +13,7 @@ from sys import platform
13
13
  from typing import Union, List
14
14
  import requests
15
15
  from requests.exceptions import RequestException
16
+ import yaml
16
17
 
17
18
  from rich.progress import Progress, SpinnerColumn, TextColumn
18
19
  from snapctl.commands.snapend import Snapend
@@ -27,7 +28,8 @@ from snapctl.config.constants import HTTP_ERROR_SERVICE_VERSION_EXISTS, \
27
28
  SNAPCTL_BYOSNAP_PUBLISH_VERSION_ERROR, HTTP_ERROR_SERVICE_IN_USE, \
28
29
  SNAPCTL_BYOSNAP_UPDATE_VERSION_ERROR, SNAPCTL_BYOSNAP_UPDATE_VERSION_SERVICE_IN_USE_ERROR, \
29
30
  SNAPCTL_BYOSNAP_UPDATE_VERSION_TAG_ERROR, SNAPCTL_BYOSNAP_NOT_FOUND, \
30
- HTTP_ERROR_RESOURCE_NOT_FOUND, SNAPCTL_BYOSNAP_PUBLISH_ERROR
31
+ HTTP_ERROR_RESOURCE_NOT_FOUND, SNAPCTL_BYOSNAP_PUBLISH_ERROR, \
32
+ SNAPCTL_BYOSNAP_GENERATE_PROFILE_ERROR
31
33
  from snapctl.utils.echo import info, warning, success
32
34
  from snapctl.utils.helper import get_composite_token, snapctl_error, snapctl_success, \
33
35
  check_dockerfile_architecture
@@ -38,11 +40,21 @@ class ByoSnap:
38
40
  CLI commands exposed for a BYOSnap
39
41
  """
40
42
  ID_PREFIX = 'byosnap-'
43
+ # These are active today
41
44
  SUBCOMMANDS = [
42
- 'create', 'publish-image', 'publish-version', 'upload-docs', 'update-version',
43
- 'publish', 'sync'
45
+ 'publish', 'sync', 'upload-docs', 'generate-profile', 'validate-profile',
46
+ 'create', 'publish-image', 'publish-version', 'update-version',
44
47
  ]
45
- PROFILE_NAME = 'snapser-byosnap-profile.json'
48
+ # These are the real commands that we want to show in the help text
49
+ SHOW_SUBCOMMANDS = ['publish', 'sync', 'upload-docs',
50
+ 'generate-profile', 'validate-profile']
51
+ # These are the commands that we want to deprecate
52
+ TO_DEPRECATE_SUBCOMMANDS = [
53
+ 'create', 'publish-image', 'publish-version', 'update-version']
54
+ DEFAULT_PROFILE_NAME_JSON = 'snapser-byosnap-profile.json'
55
+ DEFAULT_PROFILE_NAME_YAML = 'snapser-byosnap-profile.yaml'
56
+ PROFILE_FILES_PATH = 'snapctl/data/profiles/'
57
+ PROFILE_FORMATS = ['json', 'yaml', 'yml']
46
58
  PLATFORMS = ['linux/arm64', 'linux/amd64']
47
59
  LANGUAGES = ['go', 'node', 'python', 'java', 'csharp', 'cpp', 'rust',
48
60
  'ruby', 'php', 'perl', 'clojure', 'lua', 'ts', 'js', 'kotlin', 'c']
@@ -55,32 +67,26 @@ class ByoSnap:
55
67
  MAX_MIN_REPLICAS = 4
56
68
 
57
69
  def __init__(
58
- self, *, subcommand: str, base_url: str, api_key: Union[str, None], sid: str,
70
+ self, *, subcommand: str, base_url: str, api_key: Union[str, None], byosnap_id: str,
59
71
  name: Union[str, None] = None, desc: Union[str, None] = None,
60
72
  platform_type: Union[str, None] = None, language: Union[str, None] = None,
61
73
  tag: Union[str, None] = None, path: Union[str, None] = None,
62
- resources_path: Union[str, None] = None, docker_file: Union[str, None] = None,
74
+ resources_path: Union[str, None] = None, docker_filename: Union[str, None] = None,
63
75
  version: Union[str, None] = None, skip_build: bool = False,
64
76
  snapend_id: Union[str, None] = None, blocking: bool = False,
65
- byosnap_profile_file: Union[str, None] = None
77
+ profile_filename: Union[str, None] = None,
78
+ out_path: Union[str, None] = None
66
79
  ) -> None:
67
80
  # Set the BASE variables
68
81
  self.subcommand: str = subcommand
69
82
  self.base_url: str = base_url
70
83
  self.api_key: Union[str, None] = api_key
71
- self.sid: str = sid
72
- self.token: Union[str, None] = None
73
- self.token_parts: Union[list, None] = None
74
- self.byosnap_profile: Union[str, None] = None
75
- if subcommand not in ['create', 'publish']:
76
- # Create does not need the token
77
- # Publish handles this in the publish-image subcommand
78
- self._setup_token_and_token_parts(base_url, api_key, sid)
84
+ self.byosnap_id: str = byosnap_id
79
85
  self.tag: Union[str, None] = tag
80
86
  self.path: Union[str, None] = path
81
87
  self.resources_path: Union[str, None] = resources_path
82
- self.docker_file: str = docker_file
83
- self.byosnap_profile_file: Union[str, None] = byosnap_profile_file
88
+ self.docker_filename: str = docker_filename
89
+ self.profile_filename: Union[str, None] = profile_filename
84
90
  self.version: Union[str, None] = version
85
91
  self.name: Union[str, None] = name
86
92
  self.desc: Union[str, None] = desc
@@ -89,16 +95,21 @@ class ByoSnap:
89
95
  self.snapend_id: Union[str, None] = snapend_id
90
96
  self.skip_build: bool = skip_build
91
97
  self.blocking: bool = blocking
92
- # Override
98
+ self.out_path: Union[str, None] = out_path
99
+ # Values below will be overridden
100
+ self.token: Union[str, None] = None
101
+ self.token_parts: Union[list, None] = None
102
+ self.profile_path: Union[str, None] = None
103
+ self.profile_data: Union[dict, None] = None
104
+ # These variables are here because of backward compatibility
105
+ # We now takes these inputs from the BYOSnap profile
93
106
  self.prefix: Union[str, None] = None
94
107
  self.ingress_external_port: Union[dict, None] = None
95
108
  self.ingress_internal_ports: Union[list, None] = None
96
109
  self.readiness_path: Union[str, None] = None
97
110
  self.readiness_delay: Union[str, None] = None
98
- if self.subcommand in ['publish', 'publish-version']:
99
- self._validate_and_override_properties_from_profile()
100
- # Validate the input
101
- self.validate_input()
111
+ # Setup and Validate the input
112
+ self.setup_and_validate_input()
102
113
 
103
114
  # Protected methods
104
115
  @staticmethod
@@ -123,32 +134,227 @@ class ByoSnap:
123
134
  pass
124
135
  return None
125
136
 
126
- def _setup_token_and_token_parts(self, base_url, api_key, sid) -> None:
127
- '''
128
- Setup the token and token parts for publishing and syncing
129
- '''
130
- self.token: Union[str, None] = get_composite_token(
131
- base_url, api_key,
132
- 'byosnap', {'service_id': sid}
133
- )
134
- self.token_parts: Union[list, None] = ByoSnap._get_token_values(
135
- self.token) if self.token is not None else None
136
-
137
- def _check_and_get_byosnap_profile(self) -> object:
138
- """
139
- Validate the BYOSnap profile
140
- """
141
- profile_data = None
142
- with open(self.byosnap_profile, 'rb') as file:
143
- try:
144
- profile_data = json.load(file)
145
- except json.JSONDecodeError:
146
- pass
147
- if not profile_data:
137
+ @staticmethod
138
+ def _validate_byosnap_profile_data(profile_data) -> None:
139
+ # Check for the parent fields
140
+ for field in ['name', 'description', 'platform', 'language', 'prefix',
141
+ 'readiness_probe_config', 'dev_template', 'stage_template', 'prod_template']:
142
+ if field not in profile_data:
143
+ snapctl_error(
144
+ message=f'BYOSnap profile requires {field} field. ' +
145
+ 'Please use the following command: ' +
146
+ '`snapctl byosnap generate-profile --out-path $output_path` ' +
147
+ 'to generate a new profile',
148
+ code=SNAPCTL_INPUT_ERROR
149
+ )
150
+ # Check for backward compatible fields
151
+ if 'http_port' not in profile_data and 'ingress' not in profile_data:
148
152
  snapctl_error(
149
- message='Invalid BYOSnap profile JSON. Please check the JSON structure',
153
+ message='BYOSnap profile requires an ingress field with external_port and ' +
154
+ 'internal_port as children. Note: http_port is going to be deprecated soon. ' +
155
+ 'Please use the following command: ' +
156
+ '`snapctl byosnap generate-profile --out-path $output_path` ' +
157
+ 'to generate a new profile',
158
+ code=SNAPCTL_INPUT_ERROR
159
+ )
160
+ # Name Check
161
+ if profile_data['name'] is None or profile_data['name'].strip() == '':
162
+ snapctl_error(
163
+ message='BYOSnap profile requires a non empty name value. ' +
164
+ 'Please use the following command: ' +
165
+ '`snapctl byosnap generate-profile --out-path $output_path` ' +
166
+ 'to generate a new profile',
167
+ code=SNAPCTL_INPUT_ERROR
168
+ )
169
+ # Description Check
170
+ if profile_data['description'] is None or profile_data['description'].strip() == '':
171
+ snapctl_error(
172
+ message='BYOSnap profile requires a non empty description value. ' +
173
+ 'Please use the following command: ' +
174
+ '`snapctl byosnap generate-profile --out-path $output_path` ' +
175
+ 'to generate a new profile',
176
+ code=SNAPCTL_INPUT_ERROR
177
+ )
178
+ # Platform Check
179
+ if profile_data['platform'] is None or \
180
+ profile_data['platform'].strip() not in ByoSnap.PLATFORMS:
181
+ snapctl_error(
182
+ message='Invalid platform value in BYOSnap profile. Valid values are ' +
183
+ f'{", ".join(map(str, ByoSnap.PLATFORMS))}.',
184
+ code=SNAPCTL_INPUT_ERROR
185
+ )
186
+ # Language Check
187
+ if profile_data['language'] is None or profile_data['language'].strip() == '':
188
+ snapctl_error(
189
+ message='BYOSnap profile requires a non empty language value. ' +
190
+ 'Please use the following command: ' +
191
+ '`snapctl byosnap generate-profile --out-path $output_path` ' +
192
+ 'to generate a new profile',
193
+ code=SNAPCTL_INPUT_ERROR
194
+ )
195
+ # Prefix Checks
196
+ if profile_data['prefix'] is None or profile_data['prefix'].strip() == '':
197
+ snapctl_error(
198
+ message='BYOSnap profile requires a non empty prefix value. ' +
199
+ 'Please use the following command: ' +
200
+ '`snapctl byosnap generate-profile --out-path $output_path` ' +
201
+ 'to generate a new profile',
202
+ code=SNAPCTL_INPUT_ERROR
203
+ )
204
+ if not profile_data['prefix'].strip().startswith('/') or \
205
+ profile_data['prefix'].strip().endswith('/') or \
206
+ profile_data['prefix'].strip().count('/') > 1:
207
+ snapctl_error(
208
+ message='Invalid prefix value in BYOSnap profile. ' +
209
+ 'Prefix should start with a forward slash (/) and should contain exactly one ' +
210
+ 'path segment.',
211
+ code=SNAPCTL_INPUT_ERROR
212
+ )
213
+ # HTTP Port and Ingress Checks
214
+ if 'http_port' in profile_data:
215
+ if profile_data['http_port'] is None or \
216
+ not isinstance(profile_data['http_port'], int):
217
+ snapctl_error(
218
+ message='Invalid http_port value in BYOSnap profile. ' +
219
+ 'HTTP port should be a number.',
220
+ code=SNAPCTL_INPUT_ERROR
221
+ )
222
+ warning('http_port is deprecated. Please use ingress.external_port. ' +
223
+ 'You can generate a new BYOSnap profile via the `generate` command.')
224
+ # Ingress Checks
225
+ if 'ingress' in profile_data:
226
+ if profile_data['ingress'] is None:
227
+ snapctl_error(
228
+ message='BYOSnap profile requires an ingress field with external_port and ' +
229
+ 'internal_port as children. Please use the following command: ' +
230
+ '`snapctl byosnap generate-profile --out-path $output_path` ' +
231
+ 'to generate a new profile',
232
+ code=SNAPCTL_INPUT_ERROR
233
+ )
234
+ if 'external_port' not in profile_data['ingress']:
235
+ snapctl_error(
236
+ message='BYOSnap profile requires an ingress.external_port field. ' +
237
+ 'Please use the following command: ' +
238
+ '`snapctl byosnap generate-profile --out-path $output_path` ' +
239
+ 'to generate a new profile',
240
+ code=SNAPCTL_INPUT_ERROR
241
+ )
242
+ if 'name' not in profile_data['ingress']['external_port'] or \
243
+ profile_data['ingress']['external_port']['name'] != 'http':
244
+ snapctl_error(
245
+ message='Invalid Ingress external_port value in BYOSnap profile. ' +
246
+ 'External port should have a name of http and a number port value.',
247
+ code=SNAPCTL_INPUT_ERROR
248
+ )
249
+ if 'port' not in profile_data['ingress']['external_port'] or \
250
+ profile_data['ingress']['external_port']['port'] is None or \
251
+ not isinstance(profile_data['ingress']['external_port']['port'], int):
252
+ snapctl_error(
253
+ message='Invalid Ingress external_port value in BYOSnap profile. ' +
254
+ 'External port should have a name of http and a number port value.',
255
+ code=SNAPCTL_INPUT_ERROR
256
+ )
257
+ if 'internal_ports' not in profile_data['ingress'] or \
258
+ not isinstance(profile_data['ingress']['internal_ports'], list):
259
+ snapctl_error(
260
+ message='Invalid Ingress internal_port value in BYOSnap profile. ' +
261
+ 'Internal port should be an empty list or a list of objects with name and ' +
262
+ 'port values.',
263
+ code=SNAPCTL_INPUT_ERROR
264
+ )
265
+ duplicate_name = {}
266
+ duplicate_port = {}
267
+ index = 0
268
+ for internal_port_obj in profile_data['ingress']['internal_ports']:
269
+ index += 1
270
+ if 'name' not in internal_port_obj or 'port' not in internal_port_obj:
271
+ snapctl_error(
272
+ message='Invalid Ingress internal_port value in BYOSnap profile. ' +
273
+ 'Internal port should be an object with name and port values. ' +
274
+ f"Check the internal port number #{index}.",
275
+ code=SNAPCTL_INPUT_ERROR
276
+ )
277
+ if internal_port_obj['name'] is None or internal_port_obj['name'].strip() == '':
278
+ snapctl_error(
279
+ message='Invalid Ingress internal_port value in BYOSnap profile. ' +
280
+ 'Internal port name should not be empty. ' +
281
+ f"Check the internal port number #{index}.",
282
+ code=SNAPCTL_INPUT_ERROR
283
+ )
284
+ if internal_port_obj['port'] is None or \
285
+ not isinstance(internal_port_obj['port'], int):
286
+ snapctl_error(
287
+ message='Invalid Ingress internal_port value in BYOSnap profile. ' +
288
+ 'Internal port port should be a number. ' +
289
+ f"Check the internal port number #{index}.",
290
+ code=SNAPCTL_INPUT_ERROR
291
+ )
292
+ # Confirm the name does not collide with the external port
293
+ if internal_port_obj['name'] == profile_data['ingress']['external_port']['name']:
294
+ snapctl_error("Internal port name should not be the same as " +
295
+ "the external port name. " +
296
+ f"Check the internal port number #{index}.",
297
+ SNAPCTL_INPUT_ERROR)
298
+ if internal_port_obj['port'] == profile_data['ingress']['external_port']['port']:
299
+ snapctl_error("Internal port number should not be the same as " +
300
+ "the external port number. " +
301
+ f"Check the internal port number #{index}.",
302
+ SNAPCTL_INPUT_ERROR)
303
+ if internal_port_obj['name'] in duplicate_name:
304
+ snapctl_error("Duplicate internal port name. " +
305
+ f"Check the internal port number #{index}.",
306
+ SNAPCTL_INPUT_ERROR)
307
+ if internal_port_obj['port'] in duplicate_port:
308
+ snapctl_error("Duplicate internal port number. " +
309
+ f"Check the internal port number #{index}.",
310
+ SNAPCTL_INPUT_ERROR)
311
+ duplicate_name[internal_port_obj['name']] = True
312
+ duplicate_port[internal_port_obj['port']] = True
313
+ # Readiness Probe Checks
314
+ if 'readiness_probe_config' not in profile_data:
315
+ snapctl_error(
316
+ message='BYOSnap profile requires a readiness_probe_config field. ' +
317
+ 'Please use the following command: ' +
318
+ '`snapctl byosnap generate-profile --out-path $output_path` ' +
319
+ 'to generate a new profile',
150
320
  code=SNAPCTL_INPUT_ERROR
151
321
  )
322
+ if 'initial_delay_seconds' not in profile_data['readiness_probe_config'] or \
323
+ 'path' not in profile_data['readiness_probe_config']:
324
+ snapctl_error(
325
+ message='Invalid readiness_probe_config value in BYOSnap profile. ' +
326
+ 'Readiness probe config should have an initial_delay_seconds and path value. ' +
327
+ 'Set both to null if not required.',
328
+ code=SNAPCTL_INPUT_ERROR
329
+ )
330
+ if (profile_data['readiness_probe_config']['initial_delay_seconds'] is None and
331
+ profile_data['readiness_probe_config']['path'] is not None) or \
332
+ (profile_data['readiness_probe_config']['initial_delay_seconds'] is not None and
333
+ profile_data['readiness_probe_config']['path'] is None):
334
+ snapctl_error(
335
+ message='Invalid readiness_probe_config value in BYOSnap profile. ' +
336
+ 'Readiness probe config should have both initial_delay_seconds and path values. ' +
337
+ 'One of them cannot be null. However, set both to null if not required.',
338
+ code=SNAPCTL_INPUT_ERROR
339
+ )
340
+ if profile_data['readiness_probe_config']['path'] is not None is not None:
341
+ if profile_data['readiness_probe_config']['path'].strip() == '':
342
+ snapctl_error(
343
+ "Invalid readiness_probe_config.path value. Readiness path cannot be empty",
344
+ SNAPCTL_INPUT_ERROR)
345
+ if not profile_data['readiness_probe_config']['path'].strip().startswith('/'):
346
+ snapctl_error(
347
+ "Invalid readiness_probe_config.path value. Readiness path has to start with /",
348
+ SNAPCTL_INPUT_ERROR)
349
+ if profile_data['readiness_probe_config']['initial_delay_seconds'] is not None:
350
+ if not isinstance(profile_data['readiness_probe_config']['initial_delay_seconds'], int) or \
351
+ profile_data['readiness_probe_config']['initial_delay_seconds'] < 0 or \
352
+ profile_data['readiness_probe_config']['initial_delay_seconds'] > ByoSnap.MAX_READINESS_TIMEOUT:
353
+ snapctl_error(
354
+ "Invalid readiness_probe_config.path value. " +
355
+ "Readiness delay should be between 0 " +
356
+ f"and {ByoSnap.MAX_READINESS_TIMEOUT}", SNAPCTL_INPUT_ERROR)
357
+ # Template Object Checks
152
358
  if 'dev_template' not in profile_data or \
153
359
  'stage_template' not in profile_data or \
154
360
  'prod_template' not in profile_data:
@@ -157,175 +363,224 @@ class ByoSnap:
157
363
  code=SNAPCTL_INPUT_ERROR
158
364
  )
159
365
  for profile in ['dev_template', 'stage_template', 'prod_template']:
160
- # Currently, not checking for 'min_replicas' not in profile_data[profile]
161
- if 'cpu' not in profile_data[profile] or \
162
- 'memory' not in profile_data[profile] or \
163
- 'cmd' not in profile_data[profile] or \
164
- 'args' not in profile_data[profile] or \
165
- 'env_params' not in profile_data[profile]:
166
- snapctl_error(
167
- message='Invalid BYOSnap profile JSON. Please check the JSON structure',
168
- code=SNAPCTL_INPUT_ERROR
169
- )
170
- if profile_data[profile]['cpu'] not in ByoSnap.VALID_CPU_MARKS:
366
+ # IMPORTANT: Not checking for in min_replicas for backward compatibility
367
+ for field in ['cpu', 'memory', 'cmd', 'args', 'env_params']:
368
+ if field not in profile_data[profile]:
369
+ snapctl_error(
370
+ message='Invalid BYOSnap profile JSON. ' +
371
+ f'{profile} requires cpu, memory, min_replicas, cmd, args, and ' +
372
+ 'env_params fields.',
373
+ code=SNAPCTL_INPUT_ERROR
374
+ )
375
+ if profile_data[profile]['cpu'] is None or \
376
+ profile_data[profile]['cpu'] not in ByoSnap.VALID_CPU_MARKS:
171
377
  snapctl_error(
172
378
  message='Invalid CPU value in BYOSnap profile. Valid values are' +
173
- f'{", ".join(map(str, ByoSnap.VALID_CPU_MARKS))}',
379
+ f'{", ".join(map(str, ByoSnap.VALID_CPU_MARKS))}.',
174
380
  code=SNAPCTL_INPUT_ERROR
175
381
  )
176
- if profile_data[profile]['memory'] not in ByoSnap.VALID_MEMORY_MARKS:
382
+ if profile_data[profile]['memory'] is None or \
383
+ profile_data[profile]['memory'] not in ByoSnap.VALID_MEMORY_MARKS:
177
384
  snapctl_error(
178
385
  message='Invalid Memory value in BYOSnap profile. Valid values are ' +
179
- f'{", ".join(map(str, ByoSnap.VALID_MEMORY_MARKS))}',
386
+ f'{", ".join(map(str, ByoSnap.VALID_MEMORY_MARKS))}.',
180
387
  code=SNAPCTL_INPUT_ERROR
181
388
  )
182
389
  if 'min_replicas' in profile_data[profile] and \
390
+ profile_data[profile]['min_replicas'] is not None and \
183
391
  (not isinstance(profile_data[profile]['min_replicas'], int) or
184
- int(profile_data[profile]['min_replicas']) < 0 or
185
- int(profile_data[profile]['min_replicas']) > ByoSnap.MAX_MIN_REPLICAS):
392
+ profile_data[profile]['min_replicas'] < 0 or
393
+ profile_data[profile]['min_replicas'] > ByoSnap.MAX_MIN_REPLICAS):
186
394
  snapctl_error(
187
395
  message='Invalid Min Replicas value in BYOSnap profile. ' +
188
396
  'Minimum replicas should be between 0 and ' +
189
397
  f'{ByoSnap.MAX_MIN_REPLICAS}',
190
398
  code=SNAPCTL_INPUT_ERROR
191
399
  )
192
- if 'name' not in profile_data or 'description' not in profile_data or \
193
- 'platform' not in profile_data or 'language' not in profile_data or \
194
- 'prefix' not in profile_data or \
195
- ('http_port' not in profile_data and 'ingress' not in profile_data) or \
196
- 'readiness_probe_config' not in profile_data or \
197
- 'initial_delay_seconds' not in profile_data['readiness_probe_config'] or \
198
- 'path' not in profile_data['readiness_probe_config']:
400
+ if 'min_replicas' in profile_data[profile] and \
401
+ profile_data[profile]['min_replicas'] is not None and \
402
+ isinstance(profile_data[profile]['min_replicas'], int) and \
403
+ profile_data[profile]['min_replicas'] > 1 and \
404
+ (profile == 'dev_template' or profile == 'stage_template'):
405
+ snapctl_error(
406
+ message='Invalid Min Replicas value in BYOSnap profile. ' +
407
+ 'Minimum replicas should be 1 for dev and stage templates.',
408
+ code=SNAPCTL_INPUT_ERROR
409
+ )
410
+
411
+ if profile_data[profile]['cmd'] is None:
412
+ snapctl_error(
413
+ message='Invalid CMD value in BYOSnap profile. CMD should not be an ' +
414
+ 'empty string or the command you want to run in the container.',
415
+ code=SNAPCTL_INPUT_ERROR
416
+ )
417
+ if profile_data[profile]['args'] is None or \
418
+ not isinstance(profile_data[profile]['args'], list):
419
+ snapctl_error(
420
+ message='Invalid ARGS value in BYOSnap profile. ARGS should be a ' +
421
+ 'list of arguments or an empty list if no arguments are required.',
422
+ code=SNAPCTL_INPUT_ERROR
423
+ )
424
+ if profile_data[profile]['env_params'] is None or \
425
+ not isinstance(profile_data[profile]['env_params'], list):
426
+ snapctl_error(
427
+ message='Invalid env_params value in BYOSnap profile. env_params should be a ' +
428
+ 'list of environment variables as a dict with key and value attribute. ' +
429
+ 'It can be an empty list if no environment variables are required.',
430
+ code=SNAPCTL_INPUT_ERROR
431
+ )
432
+ env_index = 0
433
+ for env_param in profile_data[profile]['env_params']:
434
+ env_index += 1
435
+ if 'key' not in env_param or 'value' not in env_param:
436
+ snapctl_error(
437
+ message='Invalid env_params value in BYOSnap profile. env_params should ' +
438
+ 'be a list of environment variables as a dict with key and value ' +
439
+ 'attribute. It can be an empty list if no environment variables ' +
440
+ 'are required. ' +
441
+ f"Check the entry {profile}.env_params #{env_index}.",
442
+ code=SNAPCTL_INPUT_ERROR
443
+ )
444
+ if env_param['key'] is None or env_param['key'].strip() == '':
445
+ snapctl_error(
446
+ message='Invalid env_params value in BYOSnap profile. env_params key ' +
447
+ 'should not be empty. ' +
448
+ f"Check the key entry at {
449
+ profile}.env_params #{env_index}.",
450
+ code=SNAPCTL_INPUT_ERROR
451
+ )
452
+ if env_param['value'] is None or env_param['value'].strip() == '':
453
+ snapctl_error(
454
+ message='Invalid env_params value in BYOSnap profile. env_params value ' +
455
+ 'should not be empty. ' +
456
+ f"Check the value entry at {
457
+ profile}.env_params #{env_index}.",
458
+ code=SNAPCTL_INPUT_ERROR
459
+ )
460
+ return profile_data
461
+
462
+ @staticmethod
463
+ def _validate_byosnap_id(byosnap_id: str) -> None:
464
+ if not byosnap_id.startswith(ByoSnap.ID_PREFIX):
199
465
  snapctl_error(
200
- message='BYOSnap profile now requires name, description, platform, ' +
201
- 'language, prefix, ingress, and readiness_probe_config fields. ' +
202
- 'Please use the following command: ' +
203
- '`snapctl generate profile --category byosnap --out-path $output_path` ' +
204
- 'to generate a new profile',
466
+ message="Invalid Snap ID. Valid Snap IDs start with " +
467
+ f"{ByoSnap.ID_PREFIX}.",
205
468
  code=SNAPCTL_INPUT_ERROR
206
469
  )
207
- return profile_data
470
+ if len(byosnap_id) > ByoSnap.SID_CHARACTER_LIMIT:
471
+ snapctl_error(
472
+ message="Invalid Snap ID. Snap ID should be less than " +
473
+ f"{ByoSnap.SID_CHARACTER_LIMIT} characters",
474
+ code=SNAPCTL_INPUT_ERROR
475
+ )
476
+
477
+ @staticmethod
478
+ def _handle_output_file(input_filepath, output_filepath) -> bool:
479
+ file_written = False
480
+ with open(input_filepath, 'r') as in_file, open(output_filepath, 'w') as outfile:
481
+ for line in in_file:
482
+ outfile.write(line)
483
+ file_written = True
484
+ return file_written
208
485
 
209
- def _validate_and_override_properties_from_profile(self) -> None:
210
- # Build your snap
211
- if self.resources_path:
212
- base_path = self.resources_path
486
+ def _get_profile_contents(self) -> dict:
487
+ """
488
+ Get the BYOSNap profile contents
489
+ based on if the user has a YAML or JSON file
490
+ """
491
+ profile_contents = {}
492
+ with open(self.profile_path, 'rb') as file:
493
+ try:
494
+ if self.profile_filename.endswith('.yaml') or\
495
+ self.profile_filename.endswith('.yml'):
496
+ yaml_content = yaml.safe_load(file)
497
+ file_contents = json.dumps(yaml_content)
498
+ profile_contents = json.loads(file_contents)
499
+ else:
500
+ profile_contents = json.load(file)
501
+ except json.JSONDecodeError:
502
+ pass
503
+ return profile_contents
504
+
505
+ def _setup_token_and_token_parts(self, base_url, api_key, byosnap_id) -> None:
506
+ '''
507
+ Setup the token and token parts for publishing and syncing
508
+ '''
509
+ self.token: Union[str, None] = get_composite_token(
510
+ base_url, api_key,
511
+ 'byosnap', {'service_id': byosnap_id}
512
+ )
513
+ self.token_parts: Union[list, None] = ByoSnap._get_token_values(
514
+ self.token) if self.token is not None else None
515
+
516
+ def _setup_and_validate_byosnap_profile_data(self) -> None:
517
+ """
518
+ Pre-Override Validator
519
+ """
520
+ # Check dependencies
521
+ if self.path is None and self.resources_path is None:
522
+ snapctl_error(
523
+ message='Either the path or resources path is required ' +
524
+ 'to import the BYOSnap profile.',
525
+ code=SNAPCTL_INPUT_ERROR
526
+ )
527
+ base_path = self.resources_path if self.resources_path else self.path
528
+ # Publish and Publish version
529
+ if not self.profile_filename:
530
+ self.profile_filename = ByoSnap.DEFAULT_PROFILE_NAME_JSON
213
531
  else:
214
- base_path = self.path
215
- if not self.byosnap_profile_file:
216
- self.byosnap_profile_file = ByoSnap.PROFILE_NAME
217
- self.byosnap_profile = os.path.join(
218
- base_path, self.byosnap_profile_file)
219
- if not os.path.isfile(self.byosnap_profile):
532
+ if not self.profile_filename.endswith('.json') and \
533
+ not self.profile_filename.endswith('.yaml') and \
534
+ not self.profile_filename.endswith('.yml'):
535
+ snapctl_error(
536
+ message='Invalid BYOSnap profile file. Please check the file extension' +
537
+ ' and ensure it is either .json, .yaml, or .yml',
538
+ code=SNAPCTL_INPUT_ERROR
539
+ )
540
+ self.profile_path = os.path.join(
541
+ base_path, self.profile_filename)
542
+ if not os.path.isfile(self.profile_path):
220
543
  snapctl_error(
221
544
  "Unable to find " +
222
- f"{self.byosnap_profile_file} at path {base_path}",
545
+ f"{self.profile_filename} at path {base_path}",
223
546
  SNAPCTL_INPUT_ERROR)
224
- info('Extracting information from BYOSnap profile at path ' +
225
- f'{self.byosnap_profile}')
226
- profile_data = self._check_and_get_byosnap_profile()
227
- self.name = profile_data['name']
228
- self.desc = profile_data['description']
229
- self.platform_type = profile_data['platform']
230
- self.language = profile_data['language']
231
- self.prefix = profile_data['prefix']
547
+ profile_data_obj = self._get_profile_contents()
548
+ if not profile_data_obj:
549
+ snapctl_error(
550
+ message='Invalid BYOSnap profile JSON. Please check the JSON structure',
551
+ code=SNAPCTL_INPUT_ERROR
552
+ )
553
+ # IMPORTANT: This is where the profile data is set and validated
554
+ self.profile_data = profile_data_obj
555
+ ByoSnap._validate_byosnap_profile_data(self.profile_data)
556
+ # End: IMPORTANT: This is where the profile data is set
557
+ # Now apply the overrides
558
+ self.name = self.profile_data['name']
559
+ self.desc = self.profile_data['description']
560
+ self.platform_type = self.profile_data['platform']
561
+ self.language = self.profile_data['language']
562
+ self.prefix = self.profile_data['prefix']
232
563
  # Setup the final ingress external port
233
564
  final_ingress_external_port = {
234
565
  'name': 'http',
235
566
  'port': None
236
567
  }
237
- if 'http_port' in profile_data:
568
+ if 'http_port' in self.profile_data:
238
569
  final_ingress_external_port = {
239
570
  'name': 'http',
240
- 'port': profile_data['http_port']
571
+ 'port': self.profile_data['http_port']
241
572
  }
242
- warning('http_port is deprecated. Please use ingress.external_port. ' +
243
- 'You can generate a new BYOSnap profile via the `generate` command.')
244
- elif 'ingress' in profile_data and 'external_port' in profile_data['ingress']:
245
- final_ingress_external_port = profile_data['ingress']['external_port']
573
+ elif 'ingress' in self.profile_data and 'external_port' in self.profile_data['ingress']:
574
+ final_ingress_external_port = self.profile_data['ingress']['external_port']
246
575
  self.ingress_external_port = final_ingress_external_port
247
576
  # Setup the final ingress internal ports
248
577
  final_ingress_internal_ports = []
249
- if 'ingress' in profile_data and 'internal_ports' in profile_data['ingress']:
250
- final_ingress_internal_ports = profile_data['ingress']['internal_ports']
578
+ if 'ingress' in self.profile_data and 'internal_ports' in self.profile_data['ingress']:
579
+ final_ingress_internal_ports = self.profile_data['ingress']['internal_ports']
251
580
  self.ingress_internal_ports = final_ingress_internal_ports
252
- self.readiness_path = profile_data['readiness_probe_config']['path']
581
+ self.readiness_path = self.profile_data['readiness_probe_config']['path']
253
582
  self.readiness_delay = \
254
- profile_data['readiness_probe_config']['initial_delay_seconds']
255
- # Validate
256
- if not self.prefix or self.prefix == '':
257
- snapctl_error("Missing prefix", SNAPCTL_INPUT_ERROR)
258
- if not self.prefix.startswith('/'):
259
- snapctl_error("Prefix should start with a forward slash (/)",
260
- SNAPCTL_INPUT_ERROR)
261
- if self.prefix.endswith('/'):
262
- snapctl_error("Prefix should not end with a forward slash (/)",
263
- SNAPCTL_INPUT_ERROR)
264
- # check that prefix does not contain multiple slashes
265
- if self.prefix.count('/') > 1:
266
- snapctl_error("Prefix should not contain multiple path segments",
267
- SNAPCTL_INPUT_ERROR)
268
- if not self.version:
269
- snapctl_error("Missing version", SNAPCTL_INPUT_ERROR)
270
- pattern = r'^v\d+\.\d+\.\d+$'
271
- if not re.match(pattern, self.version):
272
- snapctl_error("Version should be in the format vX.X.X",
273
- SNAPCTL_INPUT_ERROR)
274
- if not self.ingress_external_port['port']:
275
- snapctl_error("Missing Ingress HTTP Port",
276
- SNAPCTL_INPUT_ERROR)
277
- if not isinstance(self.ingress_external_port['port'], int):
278
- snapctl_error("Ingress external port should be a number",
279
- SNAPCTL_INPUT_ERROR)
280
- # Check internal ports
281
- duplicate_name = {}
282
- duplicate_port = {}
283
- index = 0
284
- for internal_port in self.ingress_internal_ports:
285
- if 'name' not in internal_port or 'port' not in internal_port:
286
- snapctl_error("Internal ports need a name and a port. Check internal port " +
287
- f"number {index}.",
288
- SNAPCTL_INPUT_ERROR)
289
- # Confirm the name does not collide with the external port
290
- if internal_port['name'] == self.ingress_external_port['name']:
291
- snapctl_error("Internal port name should not be the same as " +
292
- "the external port name", SNAPCTL_INPUT_ERROR)
293
- # Confirm the port does not collide with the external port
294
- if internal_port['port'] == self.ingress_external_port['port']:
295
- snapctl_error("Internal port number should not be the same as " +
296
- "the external port number", SNAPCTL_INPUT_ERROR)
297
- index += 1
298
- if not internal_port['port']:
299
- snapctl_error("Missing internal port. Check internal port " +
300
- f"number {index}",
301
- SNAPCTL_INPUT_ERROR)
302
- if not isinstance(internal_port['port'], int):
303
- snapctl_error("Internal port should be a number. Check internal port " +
304
- f"number {index}",
305
- SNAPCTL_INPUT_ERROR)
306
- if internal_port['name'] in duplicate_name:
307
- snapctl_error("Duplicate internal port name. Check internal port " +
308
- f"number {index}",
309
- SNAPCTL_INPUT_ERROR)
310
- if internal_port['port'] in duplicate_port:
311
- snapctl_error("Duplicate internal port number. Check internal port " +
312
- f"number {index}",
313
- SNAPCTL_INPUT_ERROR)
314
- duplicate_name[internal_port['name']] = True
315
- duplicate_port[internal_port['port']] = True
316
- if self.readiness_path is not None:
317
- if self.readiness_path.strip() == '':
318
- snapctl_error("Readiness path cannot be empty",
319
- SNAPCTL_INPUT_ERROR)
320
- if not self.readiness_path.strip().startswith('/'):
321
- snapctl_error("Readiness path has to start with /",
322
- SNAPCTL_INPUT_ERROR)
323
- if self.readiness_delay is not None:
324
- if self.readiness_delay < 0 or \
325
- self.readiness_delay > ByoSnap.MAX_READINESS_TIMEOUT:
326
- snapctl_error(
327
- "Readiness delay should be between 0 " +
328
- f"and {ByoSnap.MAX_READINESS_TIMEOUT}", SNAPCTL_INPUT_ERROR)
583
+ self.profile_data['readiness_probe_config']['initial_delay_seconds']
329
584
 
330
585
  def _check_dependencies(self) -> None:
331
586
  """
@@ -420,7 +675,7 @@ class ByoSnap:
420
675
 
421
676
  def _docker_build(self) -> None:
422
677
  # Get the data
423
- # image_tag = f'{self.sid}.{self.tag}'
678
+ # image_tag = f'{self.byosnap_id}.{self.tag}'
424
679
  build_platform = ByoSnap.DEFAULT_BUILD_PLATFORM
425
680
  if len(self.token_parts) == 4:
426
681
  build_platform = self.token_parts[3]
@@ -438,7 +693,7 @@ class ByoSnap:
438
693
  base_path = self.resources_path
439
694
  else:
440
695
  base_path = self.path
441
- docker_file_path = os.path.join(base_path, self.docker_file)
696
+ docker_file_path = os.path.join(base_path, self.docker_filename)
442
697
 
443
698
  # Warning check for architecture specific commands
444
699
  info(f'Building on system architecture {sys_platform.machine()}')
@@ -478,7 +733,7 @@ class ByoSnap:
478
733
  def _docker_tag(self) -> None:
479
734
  # Get the data
480
735
  ecr_repo_url = self.token_parts[0]
481
- image_tag = f'{self.sid}.{self.tag}'
736
+ image_tag = f'{self.byosnap_id}.{self.tag}'
482
737
  full_ecr_repo_url = f'{ecr_repo_url}:{image_tag}'
483
738
  progress = Progress(
484
739
  SpinnerColumn(),
@@ -525,7 +780,7 @@ class ByoSnap:
525
780
  try:
526
781
  # Push the image
527
782
  ecr_repo_url = self.token_parts[0]
528
- image_tag = f'{self.sid}.{self.tag}'
783
+ image_tag = f'{self.byosnap_id}.{self.tag}'
529
784
  full_ecr_repo_url = f'{ecr_repo_url}:{image_tag}'
530
785
  if platform == "win32":
531
786
  response = subprocess.run([
@@ -580,8 +835,8 @@ class ByoSnap:
580
835
 
581
836
  # Public methods
582
837
 
583
- # Validator
584
- def validate_input(self) -> None:
838
+ # Validate
839
+ def setup_and_validate_input(self) -> None:
585
840
  """
586
841
  Validator
587
842
  """
@@ -596,21 +851,10 @@ class ByoSnap:
596
851
  f"{', '.join(ByoSnap.SUBCOMMANDS)}.",
597
852
  code=SNAPCTL_INPUT_ERROR
598
853
  )
599
- # Validate the SID
600
- if not self.sid.startswith(ByoSnap.ID_PREFIX):
601
- snapctl_error(
602
- message="Invalid Snap ID. Valid Snap IDs start with " +
603
- f"{ByoSnap.ID_PREFIX}.",
604
- code=SNAPCTL_INPUT_ERROR
605
- )
606
- if len(self.sid) > ByoSnap.SID_CHARACTER_LIMIT:
607
- snapctl_error(
608
- message="Invalid Snap ID. Snap ID should be less than " +
609
- f"{ByoSnap.SID_CHARACTER_LIMIT} characters",
610
- code=SNAPCTL_INPUT_ERROR
611
- )
612
854
  # Validation for subcommands
613
855
  if self.subcommand == 'create':
856
+ # Validator
857
+ ByoSnap._validate_byosnap_id(self.byosnap_id)
614
858
  if self.name == '':
615
859
  snapctl_error(message="Missing name", code=SNAPCTL_INPUT_ERROR)
616
860
  if not self.language:
@@ -629,9 +873,14 @@ class ByoSnap:
629
873
  code=SNAPCTL_INPUT_ERROR
630
874
  )
631
875
  elif self.subcommand == 'publish-image':
876
+ # Setup
877
+ self._setup_token_and_token_parts(
878
+ self.base_url, self.api_key, self.byosnap_id)
879
+ # Validator
632
880
  if self.token_parts is None:
633
881
  snapctl_error('Invalid token. Please reach out to your support team.',
634
882
  SNAPCTL_INPUT_ERROR)
883
+ ByoSnap._validate_byosnap_id(self.byosnap_id)
635
884
  if not self.tag:
636
885
  snapctl_error(
637
886
  "Missing required parameter: tag", SNAPCTL_INPUT_ERROR)
@@ -648,15 +897,20 @@ class ByoSnap:
648
897
  # Check path
649
898
  if self.resources_path:
650
899
  docker_file_path = \
651
- f"{self.resources_path}/{self.docker_file}"
900
+ f"{self.resources_path}/{self.docker_filename}"
652
901
  else:
653
- docker_file_path = f"{self.path}/{self.docker_file}"
902
+ docker_file_path = f"{self.path}/{self.docker_filename}"
654
903
  if not self.skip_build and not os.path.isfile(docker_file_path):
655
904
  snapctl_error(
656
905
  "Unable to find " +
657
- f"{self.docker_file} at path {docker_file_path}",
906
+ f"{self.docker_filename} at path {docker_file_path}",
658
907
  SNAPCTL_INPUT_ERROR)
659
908
  elif self.subcommand == 'upload-docs':
909
+ # Setup
910
+ self._setup_token_and_token_parts(
911
+ self.base_url, self.api_key, self.byosnap_id)
912
+ # Validator
913
+ ByoSnap._validate_byosnap_id(self.byosnap_id)
660
914
  if self.token_parts is None:
661
915
  snapctl_error('Invalid token. Please reach out to your support team.',
662
916
  SNAPCTL_INPUT_ERROR)
@@ -673,6 +927,13 @@ class ByoSnap:
673
927
  SNAPCTL_INPUT_ERROR
674
928
  )
675
929
  elif self.subcommand == 'publish-version':
930
+ # Setup
931
+ self._setup_token_and_token_parts(
932
+ self.base_url, self.api_key, self.byosnap_id)
933
+ # Setup the profile data
934
+ self._setup_and_validate_byosnap_profile_data()
935
+ # Validator
936
+ ByoSnap._validate_byosnap_id(self.byosnap_id)
676
937
  if self.token_parts is None:
677
938
  snapctl_error('Invalid token. Please reach out to your support team.',
678
939
  SNAPCTL_INPUT_ERROR)
@@ -686,7 +947,18 @@ class ByoSnap:
686
947
  f"{ByoSnap.TAG_CHARACTER_LIMIT} characters",
687
948
  SNAPCTL_INPUT_ERROR
688
949
  )
950
+ if not self.version:
951
+ snapctl_error("Missing version", SNAPCTL_INPUT_ERROR)
952
+ pattern = r'^v\d+\.\d+\.\d+$'
953
+ if not re.match(r'^v\d+\.\d+\.\d+$', self.version):
954
+ snapctl_error("Version should be in the format vX.X.X",
955
+ SNAPCTL_INPUT_ERROR)
689
956
  elif self.subcommand == 'update-version':
957
+ # Setup
958
+ self._setup_token_and_token_parts(
959
+ self.base_url, self.api_key, self.byosnap_id)
960
+ # Validator
961
+ ByoSnap._validate_byosnap_id(self.byosnap_id)
690
962
  if self.token_parts is None:
691
963
  snapctl_error('Invalid token. Please reach out to your support team.',
692
964
  SNAPCTL_INPUT_ERROR)
@@ -709,6 +981,11 @@ class ByoSnap:
709
981
  code=SNAPCTL_INPUT_ERROR
710
982
  )
711
983
  elif self.subcommand == 'sync':
984
+ # Setup
985
+ self._setup_token_and_token_parts(
986
+ self.base_url, self.api_key, self.byosnap_id)
987
+ # Validator
988
+ ByoSnap._validate_byosnap_id(self.byosnap_id)
712
989
  if self.token_parts is None:
713
990
  snapctl_error('Invalid token. Please reach out to your support team.',
714
991
  SNAPCTL_INPUT_ERROR)
@@ -726,20 +1003,27 @@ class ByoSnap:
726
1003
  # Check path
727
1004
  if self.resources_path:
728
1005
  docker_file_path = \
729
- f"{self.resources_path}/{self.docker_file}"
1006
+ f"{self.resources_path}/{self.docker_filename}"
730
1007
  else:
731
- docker_file_path = f"{self.path}/{self.docker_file}"
1008
+ docker_file_path = f"{self.path}/{self.docker_filename}"
732
1009
  if not self.skip_build and not os.path.isfile(docker_file_path):
733
1010
  snapctl_error(
734
1011
  message="Unable to find " +
735
- f"{self.docker_file} at path {docker_file_path}",
1012
+ f"{self.docker_filename} at path {docker_file_path}",
1013
+ code=SNAPCTL_INPUT_ERROR)
1014
+ if not self.snapend_id:
1015
+ snapctl_error(
1016
+ message="Missing required parameter: snapend-id",
736
1017
  code=SNAPCTL_INPUT_ERROR)
737
1018
  elif self.subcommand == 'publish':
1019
+ # Setup the profile data
1020
+ self._setup_and_validate_byosnap_profile_data()
1021
+ # Validator
1022
+ ByoSnap._validate_byosnap_id(self.byosnap_id)
738
1023
  if not self.version:
739
1024
  snapctl_error(message="Missing version. Version should be in the format vX.X.X",
740
1025
  code=SNAPCTL_INPUT_ERROR)
741
- pattern = r'^v\d+\.\d+\.\d+$'
742
- if not re.match(pattern, self.version):
1026
+ if not re.match(r'^v\d+\.\d+\.\d+$', self.version):
743
1027
  snapctl_error(message="Version should be in the format vX.X.X",
744
1028
  code=SNAPCTL_INPUT_ERROR)
745
1029
  if not self.skip_build and not self.path:
@@ -749,14 +1033,46 @@ class ByoSnap:
749
1033
  # Check path
750
1034
  if self.resources_path:
751
1035
  docker_file_path = \
752
- f"{self.resources_path}/{self.docker_file}"
1036
+ f"{self.resources_path}/{self.docker_filename}"
753
1037
  else:
754
- docker_file_path = f"{self.path}/{self.docker_file}"
1038
+ docker_file_path = f"{self.path}/{self.docker_filename}"
755
1039
  if not self.skip_build and not os.path.isfile(docker_file_path):
756
1040
  snapctl_error(
757
1041
  message="Unable to find " +
758
- f"{self.docker_file} at path {docker_file_path}",
1042
+ f"{self.docker_filename} at path {docker_file_path}",
1043
+ code=SNAPCTL_INPUT_ERROR)
1044
+
1045
+ # Run the overrides
1046
+ elif self.subcommand == 'generate-profile':
1047
+ # Setup
1048
+ # self._setup_token_and_token_parts(
1049
+ # self.base_url, self.api_key, self.byosnap_id)
1050
+ # Validator
1051
+ if not self.out_path:
1052
+ snapctl_error(
1053
+ message='Missing required parameter: out-path. ' +
1054
+ 'Path is required for profile generation',
759
1055
  code=SNAPCTL_INPUT_ERROR)
1056
+ if not os.path.isdir(self.out_path):
1057
+ snapctl_error(
1058
+ message='Invalid out-path. ' +
1059
+ 'Path should be a directory',
1060
+ code=SNAPCTL_INPUT_ERROR)
1061
+ if self.profile_filename is not None:
1062
+ if not self.profile_filename.endswith('.json') and \
1063
+ not self.profile_filename.endswith('.yaml') and \
1064
+ not self.profile_filename.endswith('.yml'):
1065
+ snapctl_error(
1066
+ message='Invalid BYOSnap profile file. Please check the file extension' +
1067
+ ' and ensure it is either .json, .yaml, or .yml',
1068
+ code=SNAPCTL_INPUT_ERROR
1069
+ )
1070
+ elif self.subcommand == 'validate-profile':
1071
+ # # Setup
1072
+ # self._setup_token_and_token_parts(
1073
+ # self.base_url, self.api_key, self.byosnap_id)
1074
+ # Setup the profile data
1075
+ self._setup_and_validate_byosnap_profile_data()
760
1076
 
761
1077
  # Basic methods
762
1078
 
@@ -812,7 +1128,7 @@ class ByoSnap:
812
1128
  with open(swagger_file, "rb") as attachment_file:
813
1129
  url = (
814
1130
  f"{self.base_url}/v1/snapser-api/byosnaps/"
815
- f"{self.sid}/docs/{self.tag}/openapispec"
1131
+ f"{self.byosnap_id}/docs/{self.tag}/openapispec"
816
1132
  )
817
1133
  test_res = requests.post(
818
1134
  url, files={"attachment": attachment_file},
@@ -841,7 +1157,7 @@ class ByoSnap:
841
1157
  with open(readme_file, "rb") as attachment_file:
842
1158
  url = (
843
1159
  f"{self.base_url}/v1/snapser-api/byosnaps/"
844
- f"{self.sid}/docs/{self.tag}/markdown"
1160
+ f"{self.byosnap_id}/docs/{self.tag}/markdown"
845
1161
  )
846
1162
  test_res = requests.post(
847
1163
  url, files={"attachment": attachment_file},
@@ -873,7 +1189,7 @@ class ByoSnap:
873
1189
  with open(file_path, "rb") as attachment_file:
874
1190
  url = (
875
1191
  f"{self.base_url}/v1/snapser-api/byosnaps/"
876
- f"{self.sid}/docs/{self.tag}/tools"
1192
+ f"{self.byosnap_id}/docs/{self.tag}/tools"
877
1193
  )
878
1194
  test_res = requests.post(
879
1195
  url, files={"attachment": attachment_file},
@@ -911,7 +1227,7 @@ class ByoSnap:
911
1227
  progress.add_task(description='Creating your snap...', total=None)
912
1228
  try:
913
1229
  payload = {
914
- "service_id": self.sid,
1230
+ "service_id": self.byosnap_id,
915
1231
  "name": self.name,
916
1232
  "description": self.desc,
917
1233
  "platform": self.platform_type,
@@ -997,16 +1313,20 @@ class ByoSnap:
997
1313
  progress.add_task(
998
1314
  description='Publishing your snap...', total=None)
999
1315
  try:
1000
- profile_data = {}
1001
- profile_data['dev_template'] = None
1002
- profile_data['stage_template'] = None
1003
- profile_data['prod_template'] = None
1004
- with open(self.byosnap_profile, 'rb') as file:
1005
- profile_data = json.load(file)
1316
+ profile_data = self._get_profile_contents()
1317
+ dev_template = None
1318
+ if 'dev_template' in profile_data:
1319
+ dev_template = profile_data['dev_template']
1320
+ stage_template = None
1321
+ if 'stage_template' in profile_data:
1322
+ stage_template = profile_data['stage_template']
1323
+ prod_template = None
1324
+ if 'prod_template' in profile_data:
1325
+ prod_template = profile_data['prod_template']
1006
1326
  payload = {
1007
1327
  "version": self.version,
1008
1328
  "image_tag": self.tag,
1009
- "base_url": f"{self.prefix}/{self.sid}",
1329
+ "base_url": f"{self.prefix}/{self.byosnap_id}",
1010
1330
  "ingress": {
1011
1331
  "external_port": self.ingress_external_port,
1012
1332
  "internal_ports": self.ingress_internal_ports
@@ -1015,14 +1335,14 @@ class ByoSnap:
1015
1335
  "path": self.readiness_path,
1016
1336
  "initial_delay_seconds": self.readiness_delay
1017
1337
  },
1018
- "dev_template": profile_data['dev_template'],
1019
- "stage_template": profile_data['stage_template'],
1020
- "prod_template": profile_data['prod_template'],
1338
+ "dev_template": dev_template,
1339
+ "stage_template": stage_template,
1340
+ "prod_template": prod_template,
1021
1341
  # Currently not supported so we are just hardcoding an empty list
1022
1342
  "egress": {"ports": []},
1023
1343
  }
1024
1344
  res = requests.post(
1025
- f"{self.base_url}/v1/snapser-api/byosnaps/{self.sid}/versions",
1345
+ f"{self.base_url}/v1/snapser-api/byosnaps/{self.byosnap_id}/versions",
1026
1346
  json=payload, headers={'api-key': self.api_key},
1027
1347
  timeout=SERVER_CALL_TIMEOUT
1028
1348
  )
@@ -1079,7 +1399,7 @@ class ByoSnap:
1079
1399
  'image_tag': self.tag,
1080
1400
  }
1081
1401
  res = requests.patch(
1082
- f"{self.base_url}/v1/snapser-api/byosnaps/{self.sid}/versions/{self.version}",
1402
+ f"{self.base_url}/v1/snapser-api/byosnaps/{self.byosnap_id}/versions/{self.version}",
1083
1403
  json=payload, headers={'api-key': self.api_key},
1084
1404
  timeout=SERVER_CALL_TIMEOUT
1085
1405
  )
@@ -1127,7 +1447,7 @@ class ByoSnap:
1127
1447
  try:
1128
1448
  # Attempt to create a BYOSnap but no worries if it fails
1129
1449
  payload = {
1130
- "service_id": self.sid,
1450
+ "service_id": self.byosnap_id,
1131
1451
  "name": self.name,
1132
1452
  "description": self.desc,
1133
1453
  "platform": self.platform_type,
@@ -1152,7 +1472,7 @@ class ByoSnap:
1152
1472
  self.tag = self.version
1153
1473
  # Setup the token and token parts
1154
1474
  self._setup_token_and_token_parts(
1155
- self.base_url, self.api_key, self.sid)
1475
+ self.base_url, self.api_key, self.byosnap_id)
1156
1476
  # Now publish the image
1157
1477
  self.publish_image(no_exit=True)
1158
1478
  # Now publish the version
@@ -1172,7 +1492,7 @@ class ByoSnap:
1172
1492
  self.tag = f'{self.version}-{int(time.time())}'
1173
1493
  self.publish_image(no_exit=True)
1174
1494
  self.update_version(no_exit=True)
1175
- byosnap_list: str = f"{self.sid}:{self.version}"
1495
+ byosnap_list: str = f"{self.byosnap_id}:{self.version}"
1176
1496
  snapend = Snapend(
1177
1497
  subcommand='update', base_url=self.base_url, api_key=self.api_key,
1178
1498
  snapend_id=self.snapend_id, byosnaps=byosnap_list, blocking=self.blocking
@@ -1184,3 +1504,52 @@ class ByoSnap:
1184
1504
  message='Exception: Unable to update a ' +
1185
1505
  f' version for your snap. Exception: {e}',
1186
1506
  code=SNAPCTL_BYOSNAP_UPDATE_VERSION_ERROR)
1507
+
1508
+ def generate_profile(self, no_exit: bool = False) -> None:
1509
+ """
1510
+ Generate snapser-byosnap-profile.json
1511
+ """
1512
+ progress = Progress(
1513
+ SpinnerColumn(),
1514
+ TextColumn("[progress.description]{task.description}"),
1515
+ transient=True,
1516
+ )
1517
+ progress.start()
1518
+ progress.add_task(
1519
+ description='Generating BYOSnap profile...', total=None)
1520
+ try:
1521
+ if self.out_path is not None:
1522
+ file_save_path = os.path.join(
1523
+ self.out_path, self.profile_filename)
1524
+ else:
1525
+ file_save_path = os.path.join(
1526
+ os.getcwd(), self.profile_filename)
1527
+ file_written = ByoSnap._handle_output_file(
1528
+ f"{ByoSnap.PROFILE_FILES_PATH}{
1529
+ self.profile_filename}", file_save_path
1530
+ )
1531
+ if file_written:
1532
+ snapctl_success(
1533
+ message="BYOSNAP Profile generation successful. " +
1534
+ f"{self.profile_filename} saved at {
1535
+ file_save_path}",
1536
+ progress=progress,
1537
+ no_exit=no_exit
1538
+ )
1539
+ return
1540
+ except (IOError, OSError) as file_error:
1541
+ snapctl_error(
1542
+ message=f"File error: {file_error}",
1543
+ code=SNAPCTL_BYOSNAP_GENERATE_PROFILE_ERROR, progress=progress)
1544
+ snapctl_error(
1545
+ message="Failed to generate BYOSNAP Profile",
1546
+ code=SNAPCTL_BYOSNAP_GENERATE_PROFILE_ERROR,
1547
+ progress=progress
1548
+ )
1549
+
1550
+ def validate_profile(self) -> None:
1551
+ '''
1552
+ Validate the profile
1553
+ '''
1554
+ # Note all the validation is already happening in the constructor
1555
+ return snapctl_success(message='BYOSNAP profile validated.')