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

@@ -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,222 @@ 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 {profile}.env_params #{env_index}.",
449
+ code=SNAPCTL_INPUT_ERROR
450
+ )
451
+ if env_param['value'] is None or env_param['value'].strip() == '':
452
+ snapctl_error(
453
+ message='Invalid env_params value in BYOSnap profile. env_params value ' +
454
+ 'should not be empty. ' +
455
+ f"Check the value entry at {profile}.env_params #{env_index}.",
456
+ code=SNAPCTL_INPUT_ERROR
457
+ )
458
+ return profile_data
459
+
460
+ @staticmethod
461
+ def _validate_byosnap_id(byosnap_id: str) -> None:
462
+ if not byosnap_id.startswith(ByoSnap.ID_PREFIX):
199
463
  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',
464
+ message="Invalid Snap ID. Valid Snap IDs start with " +
465
+ f"{ByoSnap.ID_PREFIX}.",
205
466
  code=SNAPCTL_INPUT_ERROR
206
467
  )
207
- return profile_data
468
+ if len(byosnap_id) > ByoSnap.SID_CHARACTER_LIMIT:
469
+ snapctl_error(
470
+ message="Invalid Snap ID. Snap ID should be less than " +
471
+ f"{ByoSnap.SID_CHARACTER_LIMIT} characters",
472
+ code=SNAPCTL_INPUT_ERROR
473
+ )
474
+
475
+ @staticmethod
476
+ def _handle_output_file(input_filepath, output_filepath) -> bool:
477
+ file_written = False
478
+ with open(input_filepath, 'r') as in_file, open(output_filepath, 'w') as outfile:
479
+ for line in in_file:
480
+ outfile.write(line)
481
+ file_written = True
482
+ return file_written
208
483
 
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
484
+ def _get_profile_contents(self) -> dict:
485
+ """
486
+ Get the BYOSNap profile contents
487
+ based on if the user has a YAML or JSON file
488
+ """
489
+ profile_contents = {}
490
+ with open(self.profile_path, 'rb') as file:
491
+ try:
492
+ if self.profile_filename.endswith('.yaml') or\
493
+ self.profile_filename.endswith('.yml'):
494
+ yaml_content = yaml.safe_load(file)
495
+ file_contents = json.dumps(yaml_content)
496
+ profile_contents = json.loads(file_contents)
497
+ else:
498
+ profile_contents = json.load(file)
499
+ except json.JSONDecodeError:
500
+ pass
501
+ return profile_contents
502
+
503
+ def _setup_token_and_token_parts(self, base_url, api_key, byosnap_id) -> None:
504
+ '''
505
+ Setup the token and token parts for publishing and syncing
506
+ '''
507
+ self.token: Union[str, None] = get_composite_token(
508
+ base_url, api_key,
509
+ 'byosnap', {'service_id': byosnap_id}
510
+ )
511
+ self.token_parts: Union[list, None] = ByoSnap._get_token_values(
512
+ self.token) if self.token is not None else None
513
+
514
+ def _setup_and_validate_byosnap_profile_data(self) -> None:
515
+ """
516
+ Pre-Override Validator
517
+ """
518
+ # Check dependencies
519
+ if self.path is None and self.resources_path is None:
520
+ snapctl_error(
521
+ message='Either the path or resources path is required ' +
522
+ 'to import the BYOSnap profile.',
523
+ code=SNAPCTL_INPUT_ERROR
524
+ )
525
+ base_path = self.resources_path if self.resources_path else self.path
526
+ # Publish and Publish version
527
+ if not self.profile_filename:
528
+ self.profile_filename = ByoSnap.DEFAULT_PROFILE_NAME_JSON
213
529
  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):
530
+ if not self.profile_filename.endswith('.json') and \
531
+ not self.profile_filename.endswith('.yaml') and \
532
+ not self.profile_filename.endswith('.yml'):
533
+ snapctl_error(
534
+ message='Invalid BYOSnap profile file. Please check the file extension' +
535
+ ' and ensure it is either .json, .yaml, or .yml',
536
+ code=SNAPCTL_INPUT_ERROR
537
+ )
538
+ self.profile_path = os.path.join(
539
+ base_path, self.profile_filename)
540
+ if not os.path.isfile(self.profile_path):
220
541
  snapctl_error(
221
542
  "Unable to find " +
222
- f"{self.byosnap_profile_file} at path {base_path}",
543
+ f"{self.profile_filename} at path {base_path}",
223
544
  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']
545
+ profile_data_obj = self._get_profile_contents()
546
+ if not profile_data_obj:
547
+ snapctl_error(
548
+ message='Invalid BYOSnap profile JSON. Please check the JSON structure',
549
+ code=SNAPCTL_INPUT_ERROR
550
+ )
551
+ # IMPORTANT: This is where the profile data is set and validated
552
+ self.profile_data = profile_data_obj
553
+ ByoSnap._validate_byosnap_profile_data(self.profile_data)
554
+ # End: IMPORTANT: This is where the profile data is set
555
+ # Now apply the overrides
556
+ self.name = self.profile_data['name']
557
+ self.desc = self.profile_data['description']
558
+ self.platform_type = self.profile_data['platform']
559
+ self.language = self.profile_data['language']
560
+ self.prefix = self.profile_data['prefix']
232
561
  # Setup the final ingress external port
233
562
  final_ingress_external_port = {
234
563
  'name': 'http',
235
564
  'port': None
236
565
  }
237
- if 'http_port' in profile_data:
566
+ if 'http_port' in self.profile_data:
238
567
  final_ingress_external_port = {
239
568
  'name': 'http',
240
- 'port': profile_data['http_port']
569
+ 'port': self.profile_data['http_port']
241
570
  }
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']
571
+ elif 'ingress' in self.profile_data and 'external_port' in self.profile_data['ingress']:
572
+ final_ingress_external_port = self.profile_data['ingress']['external_port']
246
573
  self.ingress_external_port = final_ingress_external_port
247
574
  # Setup the final ingress internal ports
248
575
  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']
576
+ if 'ingress' in self.profile_data and 'internal_ports' in self.profile_data['ingress']:
577
+ final_ingress_internal_ports = self.profile_data['ingress']['internal_ports']
251
578
  self.ingress_internal_ports = final_ingress_internal_ports
252
- self.readiness_path = profile_data['readiness_probe_config']['path']
579
+ self.readiness_path = self.profile_data['readiness_probe_config']['path']
253
580
  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)
581
+ self.profile_data['readiness_probe_config']['initial_delay_seconds']
329
582
 
330
583
  def _check_dependencies(self) -> None:
331
584
  """
@@ -420,7 +673,7 @@ class ByoSnap:
420
673
 
421
674
  def _docker_build(self) -> None:
422
675
  # Get the data
423
- # image_tag = f'{self.sid}.{self.tag}'
676
+ # image_tag = f'{self.byosnap_id}.{self.tag}'
424
677
  build_platform = ByoSnap.DEFAULT_BUILD_PLATFORM
425
678
  if len(self.token_parts) == 4:
426
679
  build_platform = self.token_parts[3]
@@ -438,7 +691,7 @@ class ByoSnap:
438
691
  base_path = self.resources_path
439
692
  else:
440
693
  base_path = self.path
441
- docker_file_path = os.path.join(base_path, self.docker_file)
694
+ docker_file_path = os.path.join(base_path, self.docker_filename)
442
695
 
443
696
  # Warning check for architecture specific commands
444
697
  info(f'Building on system architecture {sys_platform.machine()}')
@@ -478,7 +731,7 @@ class ByoSnap:
478
731
  def _docker_tag(self) -> None:
479
732
  # Get the data
480
733
  ecr_repo_url = self.token_parts[0]
481
- image_tag = f'{self.sid}.{self.tag}'
734
+ image_tag = f'{self.byosnap_id}.{self.tag}'
482
735
  full_ecr_repo_url = f'{ecr_repo_url}:{image_tag}'
483
736
  progress = Progress(
484
737
  SpinnerColumn(),
@@ -525,7 +778,7 @@ class ByoSnap:
525
778
  try:
526
779
  # Push the image
527
780
  ecr_repo_url = self.token_parts[0]
528
- image_tag = f'{self.sid}.{self.tag}'
781
+ image_tag = f'{self.byosnap_id}.{self.tag}'
529
782
  full_ecr_repo_url = f'{ecr_repo_url}:{image_tag}'
530
783
  if platform == "win32":
531
784
  response = subprocess.run([
@@ -580,8 +833,8 @@ class ByoSnap:
580
833
 
581
834
  # Public methods
582
835
 
583
- # Validator
584
- def validate_input(self) -> None:
836
+ # Validate
837
+ def setup_and_validate_input(self) -> None:
585
838
  """
586
839
  Validator
587
840
  """
@@ -596,21 +849,10 @@ class ByoSnap:
596
849
  f"{', '.join(ByoSnap.SUBCOMMANDS)}.",
597
850
  code=SNAPCTL_INPUT_ERROR
598
851
  )
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
852
  # Validation for subcommands
613
853
  if self.subcommand == 'create':
854
+ # Validator
855
+ ByoSnap._validate_byosnap_id(self.byosnap_id)
614
856
  if self.name == '':
615
857
  snapctl_error(message="Missing name", code=SNAPCTL_INPUT_ERROR)
616
858
  if not self.language:
@@ -629,9 +871,14 @@ class ByoSnap:
629
871
  code=SNAPCTL_INPUT_ERROR
630
872
  )
631
873
  elif self.subcommand == 'publish-image':
874
+ # Setup
875
+ self._setup_token_and_token_parts(
876
+ self.base_url, self.api_key, self.byosnap_id)
877
+ # Validator
632
878
  if self.token_parts is None:
633
879
  snapctl_error('Invalid token. Please reach out to your support team.',
634
880
  SNAPCTL_INPUT_ERROR)
881
+ ByoSnap._validate_byosnap_id(self.byosnap_id)
635
882
  if not self.tag:
636
883
  snapctl_error(
637
884
  "Missing required parameter: tag", SNAPCTL_INPUT_ERROR)
@@ -648,15 +895,20 @@ class ByoSnap:
648
895
  # Check path
649
896
  if self.resources_path:
650
897
  docker_file_path = \
651
- f"{self.resources_path}/{self.docker_file}"
898
+ f"{self.resources_path}/{self.docker_filename}"
652
899
  else:
653
- docker_file_path = f"{self.path}/{self.docker_file}"
900
+ docker_file_path = f"{self.path}/{self.docker_filename}"
654
901
  if not self.skip_build and not os.path.isfile(docker_file_path):
655
902
  snapctl_error(
656
903
  "Unable to find " +
657
- f"{self.docker_file} at path {docker_file_path}",
904
+ f"{self.docker_filename} at path {docker_file_path}",
658
905
  SNAPCTL_INPUT_ERROR)
659
906
  elif self.subcommand == 'upload-docs':
907
+ # Setup
908
+ self._setup_token_and_token_parts(
909
+ self.base_url, self.api_key, self.byosnap_id)
910
+ # Validator
911
+ ByoSnap._validate_byosnap_id(self.byosnap_id)
660
912
  if self.token_parts is None:
661
913
  snapctl_error('Invalid token. Please reach out to your support team.',
662
914
  SNAPCTL_INPUT_ERROR)
@@ -673,6 +925,13 @@ class ByoSnap:
673
925
  SNAPCTL_INPUT_ERROR
674
926
  )
675
927
  elif self.subcommand == 'publish-version':
928
+ # Setup
929
+ self._setup_token_and_token_parts(
930
+ self.base_url, self.api_key, self.byosnap_id)
931
+ # Setup the profile data
932
+ self._setup_and_validate_byosnap_profile_data()
933
+ # Validator
934
+ ByoSnap._validate_byosnap_id(self.byosnap_id)
676
935
  if self.token_parts is None:
677
936
  snapctl_error('Invalid token. Please reach out to your support team.',
678
937
  SNAPCTL_INPUT_ERROR)
@@ -686,7 +945,18 @@ class ByoSnap:
686
945
  f"{ByoSnap.TAG_CHARACTER_LIMIT} characters",
687
946
  SNAPCTL_INPUT_ERROR
688
947
  )
948
+ if not self.version:
949
+ snapctl_error("Missing version", SNAPCTL_INPUT_ERROR)
950
+ pattern = r'^v\d+\.\d+\.\d+$'
951
+ if not re.match(r'^v\d+\.\d+\.\d+$', self.version):
952
+ snapctl_error("Version should be in the format vX.X.X",
953
+ SNAPCTL_INPUT_ERROR)
689
954
  elif self.subcommand == 'update-version':
955
+ # Setup
956
+ self._setup_token_and_token_parts(
957
+ self.base_url, self.api_key, self.byosnap_id)
958
+ # Validator
959
+ ByoSnap._validate_byosnap_id(self.byosnap_id)
690
960
  if self.token_parts is None:
691
961
  snapctl_error('Invalid token. Please reach out to your support team.',
692
962
  SNAPCTL_INPUT_ERROR)
@@ -709,6 +979,11 @@ class ByoSnap:
709
979
  code=SNAPCTL_INPUT_ERROR
710
980
  )
711
981
  elif self.subcommand == 'sync':
982
+ # Setup
983
+ self._setup_token_and_token_parts(
984
+ self.base_url, self.api_key, self.byosnap_id)
985
+ # Validator
986
+ ByoSnap._validate_byosnap_id(self.byosnap_id)
712
987
  if self.token_parts is None:
713
988
  snapctl_error('Invalid token. Please reach out to your support team.',
714
989
  SNAPCTL_INPUT_ERROR)
@@ -726,20 +1001,27 @@ class ByoSnap:
726
1001
  # Check path
727
1002
  if self.resources_path:
728
1003
  docker_file_path = \
729
- f"{self.resources_path}/{self.docker_file}"
1004
+ f"{self.resources_path}/{self.docker_filename}"
730
1005
  else:
731
- docker_file_path = f"{self.path}/{self.docker_file}"
1006
+ docker_file_path = f"{self.path}/{self.docker_filename}"
732
1007
  if not self.skip_build and not os.path.isfile(docker_file_path):
733
1008
  snapctl_error(
734
1009
  message="Unable to find " +
735
- f"{self.docker_file} at path {docker_file_path}",
1010
+ f"{self.docker_filename} at path {docker_file_path}",
1011
+ code=SNAPCTL_INPUT_ERROR)
1012
+ if not self.snapend_id:
1013
+ snapctl_error(
1014
+ message="Missing required parameter: snapend-id",
736
1015
  code=SNAPCTL_INPUT_ERROR)
737
1016
  elif self.subcommand == 'publish':
1017
+ # Setup the profile data
1018
+ self._setup_and_validate_byosnap_profile_data()
1019
+ # Validator
1020
+ ByoSnap._validate_byosnap_id(self.byosnap_id)
738
1021
  if not self.version:
739
1022
  snapctl_error(message="Missing version. Version should be in the format vX.X.X",
740
1023
  code=SNAPCTL_INPUT_ERROR)
741
- pattern = r'^v\d+\.\d+\.\d+$'
742
- if not re.match(pattern, self.version):
1024
+ if not re.match(r'^v\d+\.\d+\.\d+$', self.version):
743
1025
  snapctl_error(message="Version should be in the format vX.X.X",
744
1026
  code=SNAPCTL_INPUT_ERROR)
745
1027
  if not self.skip_build and not self.path:
@@ -749,14 +1031,46 @@ class ByoSnap:
749
1031
  # Check path
750
1032
  if self.resources_path:
751
1033
  docker_file_path = \
752
- f"{self.resources_path}/{self.docker_file}"
1034
+ f"{self.resources_path}/{self.docker_filename}"
753
1035
  else:
754
- docker_file_path = f"{self.path}/{self.docker_file}"
1036
+ docker_file_path = f"{self.path}/{self.docker_filename}"
755
1037
  if not self.skip_build and not os.path.isfile(docker_file_path):
756
1038
  snapctl_error(
757
1039
  message="Unable to find " +
758
- f"{self.docker_file} at path {docker_file_path}",
1040
+ f"{self.docker_filename} at path {docker_file_path}",
1041
+ code=SNAPCTL_INPUT_ERROR)
1042
+
1043
+ # Run the overrides
1044
+ elif self.subcommand == 'generate-profile':
1045
+ # Setup
1046
+ # self._setup_token_and_token_parts(
1047
+ # self.base_url, self.api_key, self.byosnap_id)
1048
+ # Validator
1049
+ if not self.out_path:
1050
+ snapctl_error(
1051
+ message='Missing required parameter: out-path. ' +
1052
+ 'Path is required for profile generation',
759
1053
  code=SNAPCTL_INPUT_ERROR)
1054
+ if not os.path.isdir(self.out_path):
1055
+ snapctl_error(
1056
+ message='Invalid out-path. ' +
1057
+ 'Path should be a directory',
1058
+ code=SNAPCTL_INPUT_ERROR)
1059
+ if self.profile_filename is not None:
1060
+ if not self.profile_filename.endswith('.json') and \
1061
+ not self.profile_filename.endswith('.yaml') and \
1062
+ not self.profile_filename.endswith('.yml'):
1063
+ snapctl_error(
1064
+ message='Invalid BYOSnap profile file. Please check the file extension' +
1065
+ ' and ensure it is either .json, .yaml, or .yml',
1066
+ code=SNAPCTL_INPUT_ERROR
1067
+ )
1068
+ elif self.subcommand == 'validate-profile':
1069
+ # # Setup
1070
+ # self._setup_token_and_token_parts(
1071
+ # self.base_url, self.api_key, self.byosnap_id)
1072
+ # Setup the profile data
1073
+ self._setup_and_validate_byosnap_profile_data()
760
1074
 
761
1075
  # Basic methods
762
1076
 
@@ -812,7 +1126,7 @@ class ByoSnap:
812
1126
  with open(swagger_file, "rb") as attachment_file:
813
1127
  url = (
814
1128
  f"{self.base_url}/v1/snapser-api/byosnaps/"
815
- f"{self.sid}/docs/{self.tag}/openapispec"
1129
+ f"{self.byosnap_id}/docs/{self.tag}/openapispec"
816
1130
  )
817
1131
  test_res = requests.post(
818
1132
  url, files={"attachment": attachment_file},
@@ -841,7 +1155,7 @@ class ByoSnap:
841
1155
  with open(readme_file, "rb") as attachment_file:
842
1156
  url = (
843
1157
  f"{self.base_url}/v1/snapser-api/byosnaps/"
844
- f"{self.sid}/docs/{self.tag}/markdown"
1158
+ f"{self.byosnap_id}/docs/{self.tag}/markdown"
845
1159
  )
846
1160
  test_res = requests.post(
847
1161
  url, files={"attachment": attachment_file},
@@ -873,7 +1187,7 @@ class ByoSnap:
873
1187
  with open(file_path, "rb") as attachment_file:
874
1188
  url = (
875
1189
  f"{self.base_url}/v1/snapser-api/byosnaps/"
876
- f"{self.sid}/docs/{self.tag}/tools"
1190
+ f"{self.byosnap_id}/docs/{self.tag}/tools"
877
1191
  )
878
1192
  test_res = requests.post(
879
1193
  url, files={"attachment": attachment_file},
@@ -911,7 +1225,7 @@ class ByoSnap:
911
1225
  progress.add_task(description='Creating your snap...', total=None)
912
1226
  try:
913
1227
  payload = {
914
- "service_id": self.sid,
1228
+ "service_id": self.byosnap_id,
915
1229
  "name": self.name,
916
1230
  "description": self.desc,
917
1231
  "platform": self.platform_type,
@@ -997,16 +1311,20 @@ class ByoSnap:
997
1311
  progress.add_task(
998
1312
  description='Publishing your snap...', total=None)
999
1313
  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)
1314
+ profile_data = self._get_profile_contents()
1315
+ dev_template = None
1316
+ if 'dev_template' in profile_data:
1317
+ dev_template = profile_data['dev_template']
1318
+ stage_template = None
1319
+ if 'stage_template' in profile_data:
1320
+ stage_template = profile_data['stage_template']
1321
+ prod_template = None
1322
+ if 'prod_template' in profile_data:
1323
+ prod_template = profile_data['prod_template']
1006
1324
  payload = {
1007
1325
  "version": self.version,
1008
1326
  "image_tag": self.tag,
1009
- "base_url": f"{self.prefix}/{self.sid}",
1327
+ "base_url": f"{self.prefix}/{self.byosnap_id}",
1010
1328
  "ingress": {
1011
1329
  "external_port": self.ingress_external_port,
1012
1330
  "internal_ports": self.ingress_internal_ports
@@ -1015,14 +1333,14 @@ class ByoSnap:
1015
1333
  "path": self.readiness_path,
1016
1334
  "initial_delay_seconds": self.readiness_delay
1017
1335
  },
1018
- "dev_template": profile_data['dev_template'],
1019
- "stage_template": profile_data['stage_template'],
1020
- "prod_template": profile_data['prod_template'],
1336
+ "dev_template": dev_template,
1337
+ "stage_template": stage_template,
1338
+ "prod_template": prod_template,
1021
1339
  # Currently not supported so we are just hardcoding an empty list
1022
1340
  "egress": {"ports": []},
1023
1341
  }
1024
1342
  res = requests.post(
1025
- f"{self.base_url}/v1/snapser-api/byosnaps/{self.sid}/versions",
1343
+ f"{self.base_url}/v1/snapser-api/byosnaps/{self.byosnap_id}/versions",
1026
1344
  json=payload, headers={'api-key': self.api_key},
1027
1345
  timeout=SERVER_CALL_TIMEOUT
1028
1346
  )
@@ -1079,7 +1397,7 @@ class ByoSnap:
1079
1397
  'image_tag': self.tag,
1080
1398
  }
1081
1399
  res = requests.patch(
1082
- f"{self.base_url}/v1/snapser-api/byosnaps/{self.sid}/versions/{self.version}",
1400
+ f"{self.base_url}/v1/snapser-api/byosnaps/{self.byosnap_id}/versions/{self.version}",
1083
1401
  json=payload, headers={'api-key': self.api_key},
1084
1402
  timeout=SERVER_CALL_TIMEOUT
1085
1403
  )
@@ -1127,7 +1445,7 @@ class ByoSnap:
1127
1445
  try:
1128
1446
  # Attempt to create a BYOSnap but no worries if it fails
1129
1447
  payload = {
1130
- "service_id": self.sid,
1448
+ "service_id": self.byosnap_id,
1131
1449
  "name": self.name,
1132
1450
  "description": self.desc,
1133
1451
  "platform": self.platform_type,
@@ -1152,7 +1470,7 @@ class ByoSnap:
1152
1470
  self.tag = self.version
1153
1471
  # Setup the token and token parts
1154
1472
  self._setup_token_and_token_parts(
1155
- self.base_url, self.api_key, self.sid)
1473
+ self.base_url, self.api_key, self.byosnap_id)
1156
1474
  # Now publish the image
1157
1475
  self.publish_image(no_exit=True)
1158
1476
  # Now publish the version
@@ -1172,7 +1490,7 @@ class ByoSnap:
1172
1490
  self.tag = f'{self.version}-{int(time.time())}'
1173
1491
  self.publish_image(no_exit=True)
1174
1492
  self.update_version(no_exit=True)
1175
- byosnap_list: str = f"{self.sid}:{self.version}"
1493
+ byosnap_list: str = f"{self.byosnap_id}:{self.version}"
1176
1494
  snapend = Snapend(
1177
1495
  subcommand='update', base_url=self.base_url, api_key=self.api_key,
1178
1496
  snapend_id=self.snapend_id, byosnaps=byosnap_list, blocking=self.blocking
@@ -1184,3 +1502,52 @@ class ByoSnap:
1184
1502
  message='Exception: Unable to update a ' +
1185
1503
  f' version for your snap. Exception: {e}',
1186
1504
  code=SNAPCTL_BYOSNAP_UPDATE_VERSION_ERROR)
1505
+
1506
+ def generate_profile(self, no_exit: bool = False) -> None:
1507
+ """
1508
+ Generate snapser-byosnap-profile.json
1509
+ """
1510
+ progress = Progress(
1511
+ SpinnerColumn(),
1512
+ TextColumn("[progress.description]{task.description}"),
1513
+ transient=True,
1514
+ )
1515
+ progress.start()
1516
+ progress.add_task(
1517
+ description='Generating BYOSnap profile...', total=None)
1518
+ try:
1519
+ if self.out_path is not None:
1520
+ file_save_path = os.path.join(
1521
+ self.out_path, self.profile_filename)
1522
+ else:
1523
+ file_save_path = os.path.join(
1524
+ os.getcwd(), self.profile_filename)
1525
+ file_written = ByoSnap._handle_output_file(
1526
+ f"{ByoSnap.PROFILE_FILES_PATH}{
1527
+ self.profile_filename}", file_save_path
1528
+ )
1529
+ if file_written:
1530
+ snapctl_success(
1531
+ message="BYOSNAP Profile generation successful. " +
1532
+ f"{self.profile_filename} saved at {
1533
+ file_save_path}",
1534
+ progress=progress,
1535
+ no_exit=no_exit
1536
+ )
1537
+ return
1538
+ except (IOError, OSError) as file_error:
1539
+ snapctl_error(
1540
+ message=f"File error: {file_error}",
1541
+ code=SNAPCTL_BYOSNAP_GENERATE_PROFILE_ERROR, progress=progress)
1542
+ snapctl_error(
1543
+ message="Failed to generate BYOSNAP Profile",
1544
+ code=SNAPCTL_BYOSNAP_GENERATE_PROFILE_ERROR,
1545
+ progress=progress
1546
+ )
1547
+
1548
+ def validate_profile(self) -> None:
1549
+ '''
1550
+ Validate the profile
1551
+ '''
1552
+ # Note all the validation is already happening in the constructor
1553
+ return snapctl_success(message='BYOSNAP profile validated.')