snapctl 0.22.0__py3-none-any.whl → 0.22.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.

@@ -14,7 +14,8 @@ from requests.exceptions import RequestException
14
14
 
15
15
  from rich.progress import Progress, SpinnerColumn, TextColumn
16
16
  from snapctl.config.constants import SERVER_CALL_TIMEOUT
17
- from snapctl.config.constants import ERROR_SERVICE_VERSION_EXISTS, ERROR_TAG_NOT_AVAILABLE, ERROR_ADD_ON_NOT_ENABLED
17
+ from snapctl.config.constants import ERROR_SERVICE_VERSION_EXISTS, ERROR_TAG_NOT_AVAILABLE, \
18
+ ERROR_ADD_ON_NOT_ENABLED
18
19
  from snapctl.types.definitions import ResponseType
19
20
  from snapctl.utils.echo import error, success, info
20
21
  from snapctl.utils.helper import get_composite_token
@@ -25,14 +26,19 @@ class ByoSnap:
25
26
  CLI commands exposed for a BYOSnap
26
27
  """
27
28
  ID_PREFIX = 'byosnap-'
28
- SUBCOMMANDS = ['create', 'publish-image', 'publish-version', 'upload-docs']
29
+ SUBCOMMANDS = [
30
+ 'build', 'push', 'upload-docs',
31
+ 'create', 'publish-image', 'publish-version',
32
+ ]
29
33
  PLATFORMS = ['linux/arm64', 'linux/amd64']
30
34
  LANGUAGES = ['go', 'python', 'ruby', 'c#', 'c++', 'rust', 'java', 'node']
31
35
  DEFAULT_BUILD_PLATFORM = 'linux/arm64'
36
+ SID_CHARACTER_LIMIT = 47
37
+ TAG_CHARACTER_LIMIT = 80
32
38
 
33
39
  def __init__(
34
- self, subcommand: str, base_url: str, api_key: str, sid: str, name: str,
35
- desc: str, platform_type: str, language: str, tag: Union[str, None],
40
+ self, subcommand: str, base_url: str, api_key: str | None, sid: str, name: str,
41
+ desc: str, platform_type: str, language: str, input_tag: Union[str, None],
36
42
  path: Union[str, None], dockerfile: str, prefix: str, version: Union[str, None],
37
43
  http_port: Union[int, None]
38
44
  ) -> None:
@@ -45,21 +51,24 @@ class ByoSnap:
45
51
  self.platform_type: str = platform_type
46
52
  self.language: str = language
47
53
  if subcommand != 'create':
48
- self.token: Union[str, None] = get_composite_token(base_url, api_key,
49
- 'byosnap', {'service_id': sid})
54
+ self.token: Union[str, None] = get_composite_token(
55
+ base_url, api_key,
56
+ 'byosnap', {'service_id': sid}
57
+ )
50
58
  else:
51
59
  self.token: Union[str, None] = None
52
- self.token_parts: Union[list, None] = ByoSnap.get_token_values(
60
+ self.token_parts: Union[list, None] = ByoSnap._get_token_values(
53
61
  self.token) if self.token is not None else None
54
- self.tag: Union[str, None] = tag
62
+ self.input_tag: Union[str, None] = input_tag
55
63
  self.path: Union[str, None] = path
56
64
  self.dockerfile: str = dockerfile
57
65
  self.prefix: str = prefix
58
66
  self.version: Union[str, None] = version
59
67
  self.http_port: Union[int, None] = http_port
60
68
 
69
+ # Protected methods
61
70
  @staticmethod
62
- def get_token_values(token: str) -> None | list:
71
+ def _get_token_values(token: str) -> None | list:
63
72
  """
64
73
  Method to break open the token
65
74
  """
@@ -80,152 +89,10 @@ class ByoSnap:
80
89
  pass
81
90
  return None
82
91
 
83
- def validate_input(self) -> ResponseType:
92
+ def _check_dependencies(self) -> bool:
84
93
  """
85
- Validator
94
+ Check application dependencies
86
95
  """
87
- response: ResponseType = {
88
- 'error': True,
89
- 'msg': '',
90
- 'data': []
91
- }
92
- # Check subcommand
93
- if not self.subcommand in ByoSnap.SUBCOMMANDS:
94
- response['msg'] = (
95
- "Invalid command. Valid commands ",
96
- f"are {', '.join(ByoSnap.SUBCOMMANDS)}."
97
- )
98
- return response
99
- # Validate the SID
100
- if not self.sid.startswith(ByoSnap.ID_PREFIX):
101
- response['msg'] = f"Invalid Snap ID. Valid Snap IDs start with {ByoSnap.ID_PREFIX}."
102
- return response
103
- # Validation for subcommands
104
- if self.subcommand == 'create':
105
- if self.name == '':
106
- response['msg'] = "Missing name"
107
- return response
108
- if self.language not in ByoSnap.LANGUAGES:
109
- response['msg'] = (
110
- "Invalid language. Valid languages are "
111
- f"{', '.join(ByoSnap.LANGUAGES)}."
112
- )
113
- return response
114
- if self.platform_type not in ByoSnap.PLATFORMS:
115
- response['msg'] = (
116
- "Invalid platform. Valid platforms are "
117
- f"{', '.join(ByoSnap.PLATFORMS)}."
118
- )
119
- return response
120
- else:
121
- # Check the token
122
- if self.token_parts is None:
123
- response['msg'] = 'Invalid token. Please reach out to your support team.'
124
- return response
125
- # Check tag
126
- if self.tag is None or len(self.tag.split()) > 1 or len(self.tag) > 25:
127
- response['msg'] = "Tag should be a single word with maximum of 25 characters"
128
- return response
129
- if self.subcommand == 'publish-image':
130
- if not self.path:
131
- response['msg'] = "Missing required parameter: path"
132
- return response
133
- # Check path
134
- if not os.path.isfile(f"{self.path}/{self.dockerfile}"):
135
- response['msg'] = f"Unable to find {self.dockerfile} at path {self.path}"
136
- return response
137
- elif self.subcommand == 'upload-docs':
138
- if self.path is None:
139
- response['msg'] = "Missing required parameter: path"
140
- return response
141
- elif self.subcommand == 'publish-version':
142
- if not self.prefix:
143
- response['msg'] = "Missing prefix"
144
- return response
145
- if not self.version:
146
- response['msg'] = "Missing version"
147
- return response
148
- if not self.http_port:
149
- response['msg'] = "Missing Ingress HTTP Port"
150
- return response
151
- if not self.prefix.startswith('/'):
152
- response['msg'] = "Prefix should start with a forward slash (/)"
153
- return response
154
- if self.prefix.endswith('/'):
155
- response['msg'] = "Prefix should not end with a forward slash (/)"
156
- return response
157
- pattern = r'^v\d+\.\d+\.\d+$'
158
- if not re.match(pattern, self.version):
159
- response['msg'] = "Version should be in the format vX.X.X"
160
- return response
161
- if not self.http_port.isdigit():
162
- response['msg'] = "Ingress HTTP Port should be a number"
163
- return response
164
- # Send success
165
- response['error'] = False
166
- return response
167
-
168
- def create(self) -> bool:
169
- """
170
- Creating a new snap
171
- """
172
- with Progress(
173
- SpinnerColumn(),
174
- TextColumn("[progress.description]{task.description}"),
175
- transient=True,
176
- ) as progress:
177
- progress.add_task(description='Creating your snap...', total=None)
178
- try:
179
- payload = {
180
- "service_id": self.sid,
181
- "name": self.name,
182
- "description": self.desc,
183
- "platform": self.platform_type,
184
- "language": self.language,
185
- }
186
- res = requests.post(
187
- f"{self.base_url}/v1/snapser-api/byosnaps",
188
- json=payload, headers={'api-key': self.api_key},
189
- timeout=SERVER_CALL_TIMEOUT
190
- )
191
- if res.ok:
192
- return True
193
- response_json = res.json()
194
- info(response_json)
195
- if "api_error_code" in response_json and "message" in response_json:
196
- if response_json['api_error_code'] == ERROR_SERVICE_VERSION_EXISTS:
197
- error(
198
- 'Version already exists. Please update your version and try again'
199
- )
200
- elif response_json['api_error_code'] == ERROR_TAG_NOT_AVAILABLE:
201
- error('Invalid tag. Please use the correct tag')
202
- elif response_json['api_error_code'] == ERROR_ADD_ON_NOT_ENABLED:
203
- error(
204
- 'Missing Add-on. Please enable the add-on via the Snapser Web app.'
205
- )
206
- else:
207
- error(f'Server error: {response_json["message"]}')
208
- else:
209
- error(
210
- f'Server error: {json.dumps(response_json, indent=2)}'
211
- )
212
- except RequestException as e:
213
- error(f"Exception: Unable to create your snap {e}")
214
- return False
215
-
216
- def build(self) -> bool:
217
- """
218
- Build the Snap image
219
- """
220
- # Get the data
221
- ecr_repo_url = self.token_parts[0]
222
- ecr_repo_username = self.token_parts[1]
223
- ecr_repo_token = self.token_parts[2]
224
- image_tag = f'{self.sid}.{self.tag}'
225
- full_ecr_repo_url = f'{ecr_repo_url}:{image_tag}'
226
- build_platform = ByoSnap.DEFAULT_BUILD_PLATFORM
227
- if len(self.token_parts) == 4:
228
- build_platform = self.token_parts[3]
229
96
  try:
230
97
  # Check dependencies
231
98
  with Progress(
@@ -243,7 +110,19 @@ class ByoSnap:
243
110
  error('Docker not present')
244
111
  return False
245
112
  success('Dependencies Verified')
113
+ return True
114
+ except subprocess.CalledProcessError:
115
+ error('Unable to initialize docker')
116
+ return False
246
117
 
118
+ def _docker_login(self) -> bool:
119
+ """
120
+ Docker Login
121
+ """
122
+ ecr_repo_url = self.token_parts[0]
123
+ ecr_repo_username = self.token_parts[1]
124
+ ecr_repo_token = self.token_parts[2]
125
+ try:
247
126
  # Login to Snapser Registry
248
127
  with Progress(
249
128
  SpinnerColumn(),
@@ -270,7 +149,18 @@ class ByoSnap:
270
149
  )
271
150
  return False
272
151
  success('Login Successful')
152
+ return True
153
+ except subprocess.CalledProcessError:
154
+ error('Unable to initialize docker')
155
+ return False
273
156
 
157
+ def _docker_build(self) -> bool:
158
+ # Get the data
159
+ image_tag = f'{self.sid}.{self.input_tag}'
160
+ build_platform = ByoSnap.DEFAULT_BUILD_PLATFORM
161
+ if len(self.token_parts) == 4:
162
+ build_platform = self.token_parts[3]
163
+ try:
274
164
  # Build your snap
275
165
  with Progress(
276
166
  SpinnerColumn(),
@@ -295,7 +185,17 @@ class ByoSnap:
295
185
  error('Unable to build docker')
296
186
  return False
297
187
  success('Build Successful')
188
+ return True
189
+ except subprocess.CalledProcessError:
190
+ error('CLI Error')
191
+ return False
298
192
 
193
+ def _docker_tag(self) -> bool:
194
+ # Get the data
195
+ ecr_repo_url = self.token_parts[0]
196
+ image_tag = f'{self.sid}.{self.input_tag}'
197
+ full_ecr_repo_url = f'{ecr_repo_url}:{image_tag}'
198
+ try:
299
199
  # Tag the repo
300
200
  with Progress(
301
201
  SpinnerColumn(),
@@ -316,18 +216,17 @@ class ByoSnap:
316
216
  error('Unable to tag your snap')
317
217
  return False
318
218
  success('Tag Successful')
319
-
320
219
  return True
321
220
  except subprocess.CalledProcessError:
322
221
  error('CLI Error')
323
222
  return False
324
223
 
325
- def push(self) -> bool:
224
+ def _docker_push(self) -> bool:
326
225
  """
327
226
  Push the Snap image
328
227
  """
329
228
  ecr_repo_url = self.token_parts[0]
330
- image_tag = f'{self.sid}.{self.tag}'
229
+ image_tag = f'{self.sid}.{self.input_tag}'
331
230
  full_ecr_repo_url = f'{ecr_repo_url}:{image_tag}'
332
231
 
333
232
  # Push the image
@@ -353,6 +252,141 @@ class ByoSnap:
353
252
  success('Snap Upload Successful')
354
253
  return True
355
254
 
255
+ # Public methods
256
+
257
+ # Validator
258
+ def validate_input(self) -> ResponseType:
259
+ """
260
+ Validator
261
+ """
262
+ response: ResponseType = {
263
+ 'error': True,
264
+ 'msg': '',
265
+ 'data': []
266
+ }
267
+ # Check API Key and Base URL
268
+ if not self.api_key or self.base_url == '':
269
+ response['msg'] = "Missing API Key."
270
+ return response
271
+ # Check subcommand
272
+ if not self.subcommand in ByoSnap.SUBCOMMANDS:
273
+ response['msg'] = (
274
+ "Invalid command. Valid commands ",
275
+ f"are {', '.join(ByoSnap.SUBCOMMANDS)}."
276
+ )
277
+ return response
278
+ # Validate the SID
279
+ if not self.sid.startswith(ByoSnap.ID_PREFIX):
280
+ response['msg'] = f"Invalid Snap ID. Valid Snap IDs start with {ByoSnap.ID_PREFIX}."
281
+ return response
282
+ if len(self.sid) > ByoSnap.SID_CHARACTER_LIMIT:
283
+ response['msg'] = (
284
+ "Invalid Snap ID. "
285
+ f"Snap ID should be less than {ByoSnap.SID_CHARACTER_LIMIT} characters"
286
+ )
287
+ return response
288
+ # Validation for subcommands
289
+ if self.subcommand == 'create':
290
+ if self.name == '':
291
+ response['msg'] = "Missing name"
292
+ return response
293
+ if self.language not in ByoSnap.LANGUAGES:
294
+ response['msg'] = (
295
+ "Invalid language. Valid languages are "
296
+ f"{', '.join(ByoSnap.LANGUAGES)}."
297
+ )
298
+ return response
299
+ if self.platform_type not in ByoSnap.PLATFORMS:
300
+ response['msg'] = (
301
+ "Invalid platform. Valid platforms are "
302
+ f"{', '.join(ByoSnap.PLATFORMS)}."
303
+ )
304
+ return response
305
+ else:
306
+ # Check the token
307
+ if self.token_parts is None:
308
+ response['msg'] = 'Invalid token. Please reach out to your support team.'
309
+ return response
310
+ # Check tag
311
+ if self.input_tag is None or len(self.input_tag.split()) > 1 or \
312
+ len(self.input_tag) > ByoSnap.TAG_CHARACTER_LIMIT:
313
+ response['msg'] = (
314
+ "Tag should be a single word with maximum of "
315
+ f"{ByoSnap.TAG_CHARACTER_LIMIT} characters"
316
+ )
317
+ return response
318
+ if self.subcommand == 'build' or self.subcommand == 'publish-image':
319
+ if not self.input_tag:
320
+ response['msg'] = "Missing required parameter: tag"
321
+ return response
322
+ if not self.path:
323
+ response['msg'] = "Missing required parameter: path"
324
+ return response
325
+ # Check path
326
+ if not os.path.isfile(f"{self.path}/{self.dockerfile}"):
327
+ response['msg'] = f"Unable to find {self.dockerfile} at path {self.path}"
328
+ return response
329
+ elif self.subcommand == 'push':
330
+ if not self.input_tag:
331
+ response['msg'] = "Missing required parameter: tag"
332
+ return response
333
+ elif self.subcommand == 'upload-docs':
334
+ if self.path is None:
335
+ response['msg'] = "Missing required parameter: path"
336
+ return response
337
+ elif self.subcommand == 'publish-version':
338
+ if not self.prefix:
339
+ response['msg'] = "Missing prefix"
340
+ return response
341
+ if not self.version:
342
+ response['msg'] = "Missing version"
343
+ return response
344
+ if not self.http_port:
345
+ response['msg'] = "Missing Ingress HTTP Port"
346
+ return response
347
+ if not self.prefix.startswith('/'):
348
+ response['msg'] = "Prefix should start with a forward slash (/)"
349
+ return response
350
+ if self.prefix.endswith('/'):
351
+ response['msg'] = "Prefix should not end with a forward slash (/)"
352
+ return response
353
+ pattern = r'^v\d+\.\d+\.\d+$'
354
+ if not re.match(pattern, self.version):
355
+ response['msg'] = "Version should be in the format vX.X.X"
356
+ return response
357
+ if not self.http_port.isdigit():
358
+ response['msg'] = "Ingress HTTP Port should be a number"
359
+ return response
360
+ # Send success
361
+ response['error'] = False
362
+ return response
363
+
364
+ # CRUD methods
365
+ def build(self) -> bool:
366
+ """
367
+ Build the image
368
+ 1. Check Dependencies
369
+ 2. Login to Snapser Registry
370
+ 3. Build your snap
371
+ """
372
+ if not self._check_dependencies() or not self._docker_login() or \
373
+ not self._docker_build():
374
+ return False
375
+ return True
376
+
377
+ def push(self) -> bool:
378
+ """
379
+ Tag the image
380
+ 1. Check Dependencies
381
+ 2. Login to Snapser Registry
382
+ 3. Tag the snap
383
+ 4. Push your snap
384
+ """
385
+ if not self._check_dependencies() or not self._docker_login() or \
386
+ not self._docker_tag() or not self._docker_push():
387
+ return False
388
+ return True
389
+
356
390
  def upload_docs(self) -> bool:
357
391
  '''
358
392
  Note this step is optional hence we always respond with a True
@@ -372,7 +406,7 @@ class ByoSnap:
372
406
  attachment_file = open(swagger_file, "rb")
373
407
  url = (
374
408
  f"{self.base_url}/v1/snapser-api/byosnaps/"
375
- f"{self.sid}/docs/{self.tag}/openapispec"
409
+ f"{self.sid}/docs/{self.input_tag}/openapispec"
376
410
  )
377
411
  test_res = requests.post(
378
412
  url, files={"attachment": attachment_file},
@@ -405,7 +439,7 @@ class ByoSnap:
405
439
  attachment_file = open(readme_file, "rb")
406
440
  url = (
407
441
  f"{self.base_url}/v1/snapser-api/byosnaps/"
408
- f"{self.sid}/docs/{self.tag}/markdown"
442
+ f"{self.sid}/docs/{self.input_tag}/markdown"
409
443
  )
410
444
  test_res = requests.post(
411
445
  url, files={"attachment": attachment_file},
@@ -426,11 +460,68 @@ class ByoSnap:
426
460
  )
427
461
  return True
428
462
 
463
+ # Upper echelon commands
464
+ def create(self) -> bool:
465
+ """
466
+ Creating a new snap
467
+ """
468
+ with Progress(
469
+ SpinnerColumn(),
470
+ TextColumn("[progress.description]{task.description}"),
471
+ transient=True,
472
+ ) as progress:
473
+ progress.add_task(description='Creating your snap...', total=None)
474
+ try:
475
+ payload = {
476
+ "service_id": self.sid,
477
+ "name": self.name,
478
+ "description": self.desc,
479
+ "platform": self.platform_type,
480
+ "language": self.language,
481
+ }
482
+ res = requests.post(
483
+ f"{self.base_url}/v1/snapser-api/byosnaps",
484
+ json=payload, headers={'api-key': self.api_key},
485
+ timeout=SERVER_CALL_TIMEOUT
486
+ )
487
+ if res.ok:
488
+ return True
489
+ response_json = res.json()
490
+ info(response_json)
491
+ if "api_error_code" in response_json and "message" in response_json:
492
+ if response_json['api_error_code'] == ERROR_SERVICE_VERSION_EXISTS:
493
+ error(
494
+ 'Version already exists. Please update your version and try again'
495
+ )
496
+ elif response_json['api_error_code'] == ERROR_TAG_NOT_AVAILABLE:
497
+ error('Invalid tag. Please use the correct tag')
498
+ elif response_json['api_error_code'] == ERROR_ADD_ON_NOT_ENABLED:
499
+ error(
500
+ 'Missing Add-on. Please enable the add-on via the Snapser Web app.'
501
+ )
502
+ else:
503
+ error(f'Server error: {response_json["message"]}')
504
+ else:
505
+ error(
506
+ f'Server error: {json.dumps(response_json, indent=2)}'
507
+ )
508
+ except RequestException as e:
509
+ error(f"Exception: Unable to create your snap {e}")
510
+ return False
511
+
429
512
  def publish_image(self) -> bool:
430
513
  """
431
514
  Publish the image
515
+ 1. Check Dependencies
516
+ 2. Login to Snapser Registry
517
+ 3. Build your snap
518
+ 4. Tag the repo
519
+ 5. Push the image
520
+ 6. Upload swagger.json
432
521
  """
433
- if not self.build() or not self.push() or not self.upload_docs():
522
+ if not self._check_dependencies() or not self._docker_login() or \
523
+ not self._docker_build() or not self._docker_tag() or not self._docker_push() or \
524
+ not self.upload_docs():
434
525
  return False
435
526
  return True
436
527
 
@@ -448,7 +539,7 @@ class ByoSnap:
448
539
  try:
449
540
  payload = {
450
541
  "version": self.version,
451
- "image_tag": self.tag,
542
+ "image_tag": self.input_tag,
452
543
  "base_url": f"{self.prefix}/{self.sid}",
453
544
  "http_port": self.http_port,
454
545
  }
@@ -30,7 +30,7 @@ class Snapend:
30
30
  MAX_BLOCKING_RETRIES = 24
31
31
 
32
32
  def __init__(
33
- self, subcommand: str, base_url: str, api_key: str, snapend_id: str, category: str,
33
+ self, subcommand: str, base_url: str, api_key: str | None, snapend_id: str, category: str,
34
34
  platform_type: str, auth_type: str, path: Union[str, None], snaps: Union[str, None],
35
35
  byosnaps: Union[str, None], byogs: Union[str, None], blocking: bool = False
36
36
  ) -> None:
@@ -131,6 +131,10 @@ class Snapend:
131
131
  'msg': '',
132
132
  'data': []
133
133
  }
134
+ # Check API Key and Base URL
135
+ if not self.api_key or self.base_url == '':
136
+ response['msg'] = "Missing API Key."
137
+ return response
134
138
  # Check subcommand
135
139
  if not self.subcommand in Snapend.SUBCOMMANDS:
136
140
  response['msg'] = \
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Constants used by snapctl
3
3
  """
4
- VERSION = '0.22.0'
4
+ VERSION = '0.22.1'
5
5
  CONFIG_FILE_MAC = '~/.snapser/config'
6
6
  CONFIG_FILE_WIN = '%homepath%\.snapser\config'
7
7
  DEFAULT_PROFILE = 'default'