snapctl 0.43.2__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',
150
167
  code=SNAPCTL_INPUT_ERROR
151
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',
320
+ code=SNAPCTL_INPUT_ERROR
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,171 +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
485
+
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
208
515
 
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
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
- if not self.version:
265
- snapctl_error("Missing version", SNAPCTL_INPUT_ERROR)
266
- pattern = r'^v\d+\.\d+\.\d+$'
267
- if not re.match(pattern, self.version):
268
- snapctl_error("Version should be in the format vX.X.X",
269
- SNAPCTL_INPUT_ERROR)
270
- if not self.ingress_external_port['port']:
271
- snapctl_error("Missing Ingress HTTP Port",
272
- SNAPCTL_INPUT_ERROR)
273
- if not isinstance(self.ingress_external_port['port'], int):
274
- snapctl_error("Ingress external port should be a number",
275
- SNAPCTL_INPUT_ERROR)
276
- # Check internal ports
277
- duplicate_name = {}
278
- duplicate_port = {}
279
- index = 0
280
- for internal_port in self.ingress_internal_ports:
281
- if 'name' not in internal_port or 'port' not in internal_port:
282
- snapctl_error("Internal ports need a name and a port. Check internal port " +
283
- f"number {index}.",
284
- SNAPCTL_INPUT_ERROR)
285
- # Confirm the name does not collide with the external port
286
- if internal_port['name'] == self.ingress_external_port['name']:
287
- snapctl_error("Internal port name should not be the same as " +
288
- "the external port name", SNAPCTL_INPUT_ERROR)
289
- # Confirm the port does not collide with the external port
290
- if internal_port['port'] == self.ingress_external_port['port']:
291
- snapctl_error("Internal port number should not be the same as " +
292
- "the external port number", SNAPCTL_INPUT_ERROR)
293
- index += 1
294
- if not internal_port['port']:
295
- snapctl_error("Missing internal port. Check internal port " +
296
- f"number {index}",
297
- SNAPCTL_INPUT_ERROR)
298
- if not isinstance(internal_port['port'], int):
299
- snapctl_error("Internal port should be a number. Check internal port " +
300
- f"number {index}",
301
- SNAPCTL_INPUT_ERROR)
302
- if internal_port['name'] in duplicate_name:
303
- snapctl_error("Duplicate internal port name. Check internal port " +
304
- f"number {index}",
305
- SNAPCTL_INPUT_ERROR)
306
- if internal_port['port'] in duplicate_port:
307
- snapctl_error("Duplicate internal port number. Check internal port " +
308
- f"number {index}",
309
- SNAPCTL_INPUT_ERROR)
310
- duplicate_name[internal_port['name']] = True
311
- duplicate_port[internal_port['port']] = True
312
- if self.readiness_path is not None:
313
- if self.readiness_path.strip() == '':
314
- snapctl_error("Readiness path cannot be empty",
315
- SNAPCTL_INPUT_ERROR)
316
- if not self.readiness_path.strip().startswith('/'):
317
- snapctl_error("Readiness path has to start with /",
318
- SNAPCTL_INPUT_ERROR)
319
- if self.readiness_delay is not None:
320
- if self.readiness_delay < 0 or \
321
- self.readiness_delay > ByoSnap.MAX_READINESS_TIMEOUT:
322
- snapctl_error(
323
- "Readiness delay should be between 0 " +
324
- f"and {ByoSnap.MAX_READINESS_TIMEOUT}", SNAPCTL_INPUT_ERROR)
583
+ self.profile_data['readiness_probe_config']['initial_delay_seconds']
325
584
 
326
585
  def _check_dependencies(self) -> None:
327
586
  """
@@ -372,6 +631,25 @@ class ByoSnap:
372
631
  try:
373
632
  # Login to Snapser Registry
374
633
  if platform == 'win32':
634
+ # Start: Hack for Windows
635
+ data = {
636
+ "auths": {
637
+ "https://index.docker.io/v1/": {}
638
+ }
639
+ }
640
+
641
+ # Path to the Docker config file, adjust the path as necessary
642
+ docker_config_path = os.path.expanduser(
643
+ '~\\.docker\\config.json')
644
+
645
+ # Ensure the directory exists
646
+ os.makedirs(os.path.dirname(docker_config_path), exist_ok=True)
647
+
648
+ # Write the data to the file
649
+ with open(docker_config_path, 'w', encoding='utf-8') as file:
650
+ json.dump(data, file)
651
+ info("Updated the docker config for docker login")
652
+ # End: Hack for Windows
375
653
  response = subprocess.run([
376
654
  'docker', 'login', '--username', ecr_repo_username,
377
655
  '--password', ecr_repo_token, ecr_repo_url
@@ -397,7 +675,7 @@ class ByoSnap:
397
675
 
398
676
  def _docker_build(self) -> None:
399
677
  # Get the data
400
- # image_tag = f'{self.sid}.{self.tag}'
678
+ # image_tag = f'{self.byosnap_id}.{self.tag}'
401
679
  build_platform = ByoSnap.DEFAULT_BUILD_PLATFORM
402
680
  if len(self.token_parts) == 4:
403
681
  build_platform = self.token_parts[3]
@@ -415,7 +693,7 @@ class ByoSnap:
415
693
  base_path = self.resources_path
416
694
  else:
417
695
  base_path = self.path
418
- docker_file_path = os.path.join(base_path, self.docker_file)
696
+ docker_file_path = os.path.join(base_path, self.docker_filename)
419
697
 
420
698
  # Warning check for architecture specific commands
421
699
  info(f'Building on system architecture {sys_platform.machine()}')
@@ -455,7 +733,7 @@ class ByoSnap:
455
733
  def _docker_tag(self) -> None:
456
734
  # Get the data
457
735
  ecr_repo_url = self.token_parts[0]
458
- image_tag = f'{self.sid}.{self.tag}'
736
+ image_tag = f'{self.byosnap_id}.{self.tag}'
459
737
  full_ecr_repo_url = f'{ecr_repo_url}:{image_tag}'
460
738
  progress = Progress(
461
739
  SpinnerColumn(),
@@ -502,7 +780,7 @@ class ByoSnap:
502
780
  try:
503
781
  # Push the image
504
782
  ecr_repo_url = self.token_parts[0]
505
- image_tag = f'{self.sid}.{self.tag}'
783
+ image_tag = f'{self.byosnap_id}.{self.tag}'
506
784
  full_ecr_repo_url = f'{ecr_repo_url}:{image_tag}'
507
785
  if platform == "win32":
508
786
  response = subprocess.run([
@@ -557,8 +835,8 @@ class ByoSnap:
557
835
 
558
836
  # Public methods
559
837
 
560
- # Validator
561
- def validate_input(self) -> None:
838
+ # Validate
839
+ def setup_and_validate_input(self) -> None:
562
840
  """
563
841
  Validator
564
842
  """
@@ -573,21 +851,10 @@ class ByoSnap:
573
851
  f"{', '.join(ByoSnap.SUBCOMMANDS)}.",
574
852
  code=SNAPCTL_INPUT_ERROR
575
853
  )
576
- # Validate the SID
577
- if not self.sid.startswith(ByoSnap.ID_PREFIX):
578
- snapctl_error(
579
- message="Invalid Snap ID. Valid Snap IDs start with " +
580
- f"{ByoSnap.ID_PREFIX}.",
581
- code=SNAPCTL_INPUT_ERROR
582
- )
583
- if len(self.sid) > ByoSnap.SID_CHARACTER_LIMIT:
584
- snapctl_error(
585
- message="Invalid Snap ID. Snap ID should be less than " +
586
- f"{ByoSnap.SID_CHARACTER_LIMIT} characters",
587
- code=SNAPCTL_INPUT_ERROR
588
- )
589
854
  # Validation for subcommands
590
855
  if self.subcommand == 'create':
856
+ # Validator
857
+ ByoSnap._validate_byosnap_id(self.byosnap_id)
591
858
  if self.name == '':
592
859
  snapctl_error(message="Missing name", code=SNAPCTL_INPUT_ERROR)
593
860
  if not self.language:
@@ -606,9 +873,14 @@ class ByoSnap:
606
873
  code=SNAPCTL_INPUT_ERROR
607
874
  )
608
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
609
880
  if self.token_parts is None:
610
881
  snapctl_error('Invalid token. Please reach out to your support team.',
611
882
  SNAPCTL_INPUT_ERROR)
883
+ ByoSnap._validate_byosnap_id(self.byosnap_id)
612
884
  if not self.tag:
613
885
  snapctl_error(
614
886
  "Missing required parameter: tag", SNAPCTL_INPUT_ERROR)
@@ -625,22 +897,43 @@ class ByoSnap:
625
897
  # Check path
626
898
  if self.resources_path:
627
899
  docker_file_path = \
628
- f"{self.resources_path}/{self.docker_file}"
900
+ f"{self.resources_path}/{self.docker_filename}"
629
901
  else:
630
- docker_file_path = f"{self.path}/{self.docker_file}"
902
+ docker_file_path = f"{self.path}/{self.docker_filename}"
631
903
  if not self.skip_build and not os.path.isfile(docker_file_path):
632
904
  snapctl_error(
633
905
  "Unable to find " +
634
- f"{self.docker_file} at path {docker_file_path}",
906
+ f"{self.docker_filename} at path {docker_file_path}",
635
907
  SNAPCTL_INPUT_ERROR)
636
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)
637
914
  if self.token_parts is None:
638
915
  snapctl_error('Invalid token. Please reach out to your support team.',
639
916
  SNAPCTL_INPUT_ERROR)
640
917
  if self.path is None and self.resources_path is None:
641
918
  snapctl_error(
642
919
  "Missing one of: path or resources-path parameter", SNAPCTL_INPUT_ERROR)
920
+ if not self.tag:
921
+ snapctl_error("Missing tag", SNAPCTL_INPUT_ERROR)
922
+ if len(self.tag.split()) > 1 or \
923
+ len(self.tag) > ByoSnap.TAG_CHARACTER_LIMIT:
924
+ snapctl_error(
925
+ "Tag should be a single word with maximum of " +
926
+ f"{ByoSnap.TAG_CHARACTER_LIMIT} characters",
927
+ SNAPCTL_INPUT_ERROR
928
+ )
643
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)
644
937
  if self.token_parts is None:
645
938
  snapctl_error('Invalid token. Please reach out to your support team.',
646
939
  SNAPCTL_INPUT_ERROR)
@@ -654,7 +947,18 @@ class ByoSnap:
654
947
  f"{ByoSnap.TAG_CHARACTER_LIMIT} characters",
655
948
  SNAPCTL_INPUT_ERROR
656
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)
657
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)
658
962
  if self.token_parts is None:
659
963
  snapctl_error('Invalid token. Please reach out to your support team.',
660
964
  SNAPCTL_INPUT_ERROR)
@@ -677,6 +981,11 @@ class ByoSnap:
677
981
  code=SNAPCTL_INPUT_ERROR
678
982
  )
679
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)
680
989
  if self.token_parts is None:
681
990
  snapctl_error('Invalid token. Please reach out to your support team.',
682
991
  SNAPCTL_INPUT_ERROR)
@@ -694,20 +1003,27 @@ class ByoSnap:
694
1003
  # Check path
695
1004
  if self.resources_path:
696
1005
  docker_file_path = \
697
- f"{self.resources_path}/{self.docker_file}"
1006
+ f"{self.resources_path}/{self.docker_filename}"
698
1007
  else:
699
- docker_file_path = f"{self.path}/{self.docker_file}"
1008
+ docker_file_path = f"{self.path}/{self.docker_filename}"
700
1009
  if not self.skip_build and not os.path.isfile(docker_file_path):
701
1010
  snapctl_error(
702
1011
  message="Unable to find " +
703
- 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",
704
1017
  code=SNAPCTL_INPUT_ERROR)
705
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)
706
1023
  if not self.version:
707
1024
  snapctl_error(message="Missing version. Version should be in the format vX.X.X",
708
1025
  code=SNAPCTL_INPUT_ERROR)
709
- pattern = r'^v\d+\.\d+\.\d+$'
710
- if not re.match(pattern, self.version):
1026
+ if not re.match(r'^v\d+\.\d+\.\d+$', self.version):
711
1027
  snapctl_error(message="Version should be in the format vX.X.X",
712
1028
  code=SNAPCTL_INPUT_ERROR)
713
1029
  if not self.skip_build and not self.path:
@@ -717,15 +1033,47 @@ class ByoSnap:
717
1033
  # Check path
718
1034
  if self.resources_path:
719
1035
  docker_file_path = \
720
- f"{self.resources_path}/{self.docker_file}"
1036
+ f"{self.resources_path}/{self.docker_filename}"
721
1037
  else:
722
- docker_file_path = f"{self.path}/{self.docker_file}"
1038
+ docker_file_path = f"{self.path}/{self.docker_filename}"
723
1039
  if not self.skip_build and not os.path.isfile(docker_file_path):
724
1040
  snapctl_error(
725
1041
  message="Unable to find " +
726
- f"{self.docker_file} at path {docker_file_path}",
1042
+ f"{self.docker_filename} at path {docker_file_path}",
727
1043
  code=SNAPCTL_INPUT_ERROR)
728
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',
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()
1076
+
729
1077
  # Basic methods
730
1078
 
731
1079
  def build(self) -> None:
@@ -780,7 +1128,7 @@ class ByoSnap:
780
1128
  with open(swagger_file, "rb") as attachment_file:
781
1129
  url = (
782
1130
  f"{self.base_url}/v1/snapser-api/byosnaps/"
783
- f"{self.sid}/docs/{self.tag}/openapispec"
1131
+ f"{self.byosnap_id}/docs/{self.tag}/openapispec"
784
1132
  )
785
1133
  test_res = requests.post(
786
1134
  url, files={"attachment": attachment_file},
@@ -809,7 +1157,7 @@ class ByoSnap:
809
1157
  with open(readme_file, "rb") as attachment_file:
810
1158
  url = (
811
1159
  f"{self.base_url}/v1/snapser-api/byosnaps/"
812
- f"{self.sid}/docs/{self.tag}/markdown"
1160
+ f"{self.byosnap_id}/docs/{self.tag}/markdown"
813
1161
  )
814
1162
  test_res = requests.post(
815
1163
  url, files={"attachment": attachment_file},
@@ -841,7 +1189,7 @@ class ByoSnap:
841
1189
  with open(file_path, "rb") as attachment_file:
842
1190
  url = (
843
1191
  f"{self.base_url}/v1/snapser-api/byosnaps/"
844
- f"{self.sid}/docs/{self.tag}/tools"
1192
+ f"{self.byosnap_id}/docs/{self.tag}/tools"
845
1193
  )
846
1194
  test_res = requests.post(
847
1195
  url, files={"attachment": attachment_file},
@@ -879,7 +1227,7 @@ class ByoSnap:
879
1227
  progress.add_task(description='Creating your snap...', total=None)
880
1228
  try:
881
1229
  payload = {
882
- "service_id": self.sid,
1230
+ "service_id": self.byosnap_id,
883
1231
  "name": self.name,
884
1232
  "description": self.desc,
885
1233
  "platform": self.platform_type,
@@ -965,16 +1313,20 @@ class ByoSnap:
965
1313
  progress.add_task(
966
1314
  description='Publishing your snap...', total=None)
967
1315
  try:
968
- profile_data = {}
969
- profile_data['dev_template'] = None
970
- profile_data['stage_template'] = None
971
- profile_data['prod_template'] = None
972
- with open(self.byosnap_profile, 'rb') as file:
973
- 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']
974
1326
  payload = {
975
1327
  "version": self.version,
976
1328
  "image_tag": self.tag,
977
- "base_url": f"{self.prefix}/{self.sid}",
1329
+ "base_url": f"{self.prefix}/{self.byosnap_id}",
978
1330
  "ingress": {
979
1331
  "external_port": self.ingress_external_port,
980
1332
  "internal_ports": self.ingress_internal_ports
@@ -983,14 +1335,14 @@ class ByoSnap:
983
1335
  "path": self.readiness_path,
984
1336
  "initial_delay_seconds": self.readiness_delay
985
1337
  },
986
- "dev_template": profile_data['dev_template'],
987
- "stage_template": profile_data['stage_template'],
988
- "prod_template": profile_data['prod_template'],
1338
+ "dev_template": dev_template,
1339
+ "stage_template": stage_template,
1340
+ "prod_template": prod_template,
989
1341
  # Currently not supported so we are just hardcoding an empty list
990
1342
  "egress": {"ports": []},
991
1343
  }
992
1344
  res = requests.post(
993
- f"{self.base_url}/v1/snapser-api/byosnaps/{self.sid}/versions",
1345
+ f"{self.base_url}/v1/snapser-api/byosnaps/{self.byosnap_id}/versions",
994
1346
  json=payload, headers={'api-key': self.api_key},
995
1347
  timeout=SERVER_CALL_TIMEOUT
996
1348
  )
@@ -1047,7 +1399,7 @@ class ByoSnap:
1047
1399
  'image_tag': self.tag,
1048
1400
  }
1049
1401
  res = requests.patch(
1050
- 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}",
1051
1403
  json=payload, headers={'api-key': self.api_key},
1052
1404
  timeout=SERVER_CALL_TIMEOUT
1053
1405
  )
@@ -1095,7 +1447,7 @@ class ByoSnap:
1095
1447
  try:
1096
1448
  # Attempt to create a BYOSnap but no worries if it fails
1097
1449
  payload = {
1098
- "service_id": self.sid,
1450
+ "service_id": self.byosnap_id,
1099
1451
  "name": self.name,
1100
1452
  "description": self.desc,
1101
1453
  "platform": self.platform_type,
@@ -1120,7 +1472,7 @@ class ByoSnap:
1120
1472
  self.tag = self.version
1121
1473
  # Setup the token and token parts
1122
1474
  self._setup_token_and_token_parts(
1123
- self.base_url, self.api_key, self.sid)
1475
+ self.base_url, self.api_key, self.byosnap_id)
1124
1476
  # Now publish the image
1125
1477
  self.publish_image(no_exit=True)
1126
1478
  # Now publish the version
@@ -1140,7 +1492,7 @@ class ByoSnap:
1140
1492
  self.tag = f'{self.version}-{int(time.time())}'
1141
1493
  self.publish_image(no_exit=True)
1142
1494
  self.update_version(no_exit=True)
1143
- byosnap_list: str = f"{self.sid}:{self.version}"
1495
+ byosnap_list: str = f"{self.byosnap_id}:{self.version}"
1144
1496
  snapend = Snapend(
1145
1497
  subcommand='update', base_url=self.base_url, api_key=self.api_key,
1146
1498
  snapend_id=self.snapend_id, byosnaps=byosnap_list, blocking=self.blocking
@@ -1152,3 +1504,52 @@ class ByoSnap:
1152
1504
  message='Exception: Unable to update a ' +
1153
1505
  f' version for your snap. Exception: {e}',
1154
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.')