snapctl 0.32.2__tar.gz → 0.38.0__tar.gz

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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: snapctl
3
- Version: 0.32.2
3
+ Version: 0.38.0
4
4
  Summary: Snapser CLI Tool
5
5
  Author: Ajinkya Apte
6
6
  Author-email: aj@snapser.com
@@ -264,6 +264,10 @@ snapctl byosnap upload-docs $byosnap_sid --tag $image_tag --path $code_root_path
264
264
  Publish a custom snap code image. This command executes, `build`, `push` and `upload-docs` one
265
265
  after the other.
266
266
 
267
+ **IMPORTANT**: Take note of the hardware architecture of machine and your Dockerfile commands.
268
+ Commands in docker file may be hardware architecture specific. Snapser throws a warning if it detects
269
+ a mismatch.
270
+
267
271
  ```
268
272
  # Help for the byosnap command
269
273
  snapctl byosnap publish-image --help
@@ -272,9 +276,11 @@ snapctl byosnap publish-image --help
272
276
  # $byosnap_sid = Snap ID for your snap
273
277
  # $image_tag = An image tag for your snap
274
278
  # $code_root_path = Local code path where your Dockerfile is present
279
+ # $skip-build = true/false. Default is false. Pass this flag as true to skip the build and head straight to tag and push. Build step needs to run and tagged using the --tag you pass to the publish-image command for this to work.
275
280
  # Example:
276
281
  # snapctl byosnap publish-image byosnap-jinks-flask --tag my-first-image --path /Users/DevName/Development/SnapserEngine/jinks_flask
277
282
  snapctl byosnap publish-image $byosnap_sid --tag $image_tag --path $code_root_path
283
+ snapctl byosnap publish-image $byosnap_sid --tag $image_tag --skip-build
278
284
  ```
279
285
 
280
286
  #### 7. byosnap publish-version
@@ -292,7 +298,7 @@ snapctl byosnap publish-version --help
292
298
  # $prefix = Prefix for your snap Eg: /v1
293
299
  # $version = Semantic version for your snap Eg: v0.0.1
294
300
  # $ingress_port = Ingress port for your snap Eg: 5003
295
- # $byosnap_profile = BYOSnap profile to configure dev, stage and prod settings for this snap. You can generate a base version of this file using the `snapctl generate byosnap` command
301
+ # $byosnap_profile = BYOSnap profile to configure dev, stage and prod settings for this snap. You can generate a base version of this file using the `snapctl generate profile --category byosnap --out-path <output_path>` command
296
302
  # Example:
297
303
  # snapctl byosnap publish-image byosnap-jinks-flask --tag my-first-image --prefix /v1 --version v0.0.1 --http-port 5003 --byosnap-profile /Users/DevName/Development/SnapserEngine/jinks_flask/snapser-byosnap-profile.json
298
304
  snapctl byosnap publish-version $byosnap_sid --tag $image_tag --prefix $prefix --version $version --http-port $ingress_port --byosnap-profile $byosnap_profile
@@ -346,6 +352,10 @@ snapctl byogs push --tag $image_tag
346
352
  Publish your custom game server image. This commend replaces the old way of creating, publishing image and
347
353
  then publishing the byogs. Now all you have to do is publish your image and create a fleet using the web portal.
348
354
 
355
+ **IMPORTANT**: Take note of the hardware architecture of machine and your Dockerfile commands.
356
+ Commands in docker file may be hardware architecture specific. Snapser throws a warning if it detects
357
+ a mismatch.
358
+
349
359
  ```
350
360
  # Help for the byogs command
351
361
  snapctl byogs publish --help
@@ -353,9 +363,11 @@ snapctl byogs publish --help
353
363
  # Publish a new image
354
364
  # $image_tag = An image tag for your snap
355
365
  # $code_root_path = Local code path where your Dockerfile is present
366
+ # $skip-build = Default is false. Pass this flag as true to skip the build and head straight to tag and push. Build step needs to run and tagged using the --tag you pass to the publish-image command for this to work.
356
367
  # Example:
357
368
  # snapctl byogs publish --tag my-first-image --path /Users/DevName/Development/SnapserEngine/game_server
358
369
  snapctl byogs publish --tag $image_tag --path $code_root_path
370
+ snapctl byogs publish --tag $image_tag --skip-build
359
371
  ```
360
372
 
361
373
 
@@ -393,11 +405,19 @@ See all the supported commands
393
405
  snapctl generate --help
394
406
  ```
395
407
 
396
- #### 2. BYOSnap Profile
408
+ #### 2. Generate BYOSnap Profile
397
409
  Generate the base file for BYOSnap profile to be used in the `snapctl byosnap publish-version` command
398
410
 
399
411
  ```
400
- snapctl generate byosnap-profile --out-path $output_path
412
+ snapctl generate profile --category "byosnap" --out-path $output_path
413
+
414
+ ```
415
+
416
+ #### 3. Generate ECR Credentials
417
+ Generate the ECR credentials. Game studios can use these credentials to self publish their images to Snapser.
418
+
419
+ ```
420
+ snapctl generate credentials --category "ecr" --out-path $output_path
401
421
 
402
422
  ```
403
423
 
@@ -596,5 +616,6 @@ snapctl snapend state $snapend_id
596
616
  | Error Code | Description |
597
617
  |------------|----------------------------------------------------------|
598
618
  | 80 | Generic generate error |
599
- | 81 | Generate BYOSNAP profile error |
619
+ | 81 | Generate profile error |
620
+ | 82 | Generate credentials error |
600
621
 
@@ -245,6 +245,10 @@ snapctl byosnap upload-docs $byosnap_sid --tag $image_tag --path $code_root_path
245
245
  Publish a custom snap code image. This command executes, `build`, `push` and `upload-docs` one
246
246
  after the other.
247
247
 
248
+ **IMPORTANT**: Take note of the hardware architecture of machine and your Dockerfile commands.
249
+ Commands in docker file may be hardware architecture specific. Snapser throws a warning if it detects
250
+ a mismatch.
251
+
248
252
  ```
249
253
  # Help for the byosnap command
250
254
  snapctl byosnap publish-image --help
@@ -253,9 +257,11 @@ snapctl byosnap publish-image --help
253
257
  # $byosnap_sid = Snap ID for your snap
254
258
  # $image_tag = An image tag for your snap
255
259
  # $code_root_path = Local code path where your Dockerfile is present
260
+ # $skip-build = true/false. Default is false. Pass this flag as true to skip the build and head straight to tag and push. Build step needs to run and tagged using the --tag you pass to the publish-image command for this to work.
256
261
  # Example:
257
262
  # snapctl byosnap publish-image byosnap-jinks-flask --tag my-first-image --path /Users/DevName/Development/SnapserEngine/jinks_flask
258
263
  snapctl byosnap publish-image $byosnap_sid --tag $image_tag --path $code_root_path
264
+ snapctl byosnap publish-image $byosnap_sid --tag $image_tag --skip-build
259
265
  ```
260
266
 
261
267
  #### 7. byosnap publish-version
@@ -273,7 +279,7 @@ snapctl byosnap publish-version --help
273
279
  # $prefix = Prefix for your snap Eg: /v1
274
280
  # $version = Semantic version for your snap Eg: v0.0.1
275
281
  # $ingress_port = Ingress port for your snap Eg: 5003
276
- # $byosnap_profile = BYOSnap profile to configure dev, stage and prod settings for this snap. You can generate a base version of this file using the `snapctl generate byosnap` command
282
+ # $byosnap_profile = BYOSnap profile to configure dev, stage and prod settings for this snap. You can generate a base version of this file using the `snapctl generate profile --category byosnap --out-path <output_path>` command
277
283
  # Example:
278
284
  # snapctl byosnap publish-image byosnap-jinks-flask --tag my-first-image --prefix /v1 --version v0.0.1 --http-port 5003 --byosnap-profile /Users/DevName/Development/SnapserEngine/jinks_flask/snapser-byosnap-profile.json
279
285
  snapctl byosnap publish-version $byosnap_sid --tag $image_tag --prefix $prefix --version $version --http-port $ingress_port --byosnap-profile $byosnap_profile
@@ -327,6 +333,10 @@ snapctl byogs push --tag $image_tag
327
333
  Publish your custom game server image. This commend replaces the old way of creating, publishing image and
328
334
  then publishing the byogs. Now all you have to do is publish your image and create a fleet using the web portal.
329
335
 
336
+ **IMPORTANT**: Take note of the hardware architecture of machine and your Dockerfile commands.
337
+ Commands in docker file may be hardware architecture specific. Snapser throws a warning if it detects
338
+ a mismatch.
339
+
330
340
  ```
331
341
  # Help for the byogs command
332
342
  snapctl byogs publish --help
@@ -334,9 +344,11 @@ snapctl byogs publish --help
334
344
  # Publish a new image
335
345
  # $image_tag = An image tag for your snap
336
346
  # $code_root_path = Local code path where your Dockerfile is present
347
+ # $skip-build = Default is false. Pass this flag as true to skip the build and head straight to tag and push. Build step needs to run and tagged using the --tag you pass to the publish-image command for this to work.
337
348
  # Example:
338
349
  # snapctl byogs publish --tag my-first-image --path /Users/DevName/Development/SnapserEngine/game_server
339
350
  snapctl byogs publish --tag $image_tag --path $code_root_path
351
+ snapctl byogs publish --tag $image_tag --skip-build
340
352
  ```
341
353
 
342
354
 
@@ -374,11 +386,19 @@ See all the supported commands
374
386
  snapctl generate --help
375
387
  ```
376
388
 
377
- #### 2. BYOSnap Profile
389
+ #### 2. Generate BYOSnap Profile
378
390
  Generate the base file for BYOSnap profile to be used in the `snapctl byosnap publish-version` command
379
391
 
380
392
  ```
381
- snapctl generate byosnap-profile --out-path $output_path
393
+ snapctl generate profile --category "byosnap" --out-path $output_path
394
+
395
+ ```
396
+
397
+ #### 3. Generate ECR Credentials
398
+ Generate the ECR credentials. Game studios can use these credentials to self publish their images to Snapser.
399
+
400
+ ```
401
+ snapctl generate credentials --category "ecr" --out-path $output_path
382
402
 
383
403
  ```
384
404
 
@@ -577,4 +597,5 @@ snapctl snapend state $snapend_id
577
597
  | Error Code | Description |
578
598
  |------------|----------------------------------------------------------|
579
599
  | 80 | Generic generate error |
580
- | 81 | Generate BYOSNAP profile error |
600
+ | 81 | Generate profile error |
601
+ | 82 | Generate credentials error |
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "snapctl"
3
- version = "0.32.2"
3
+ version = "0.38.0"
4
4
  description = "Snapser CLI Tool"
5
5
  authors = ["Ajinkya Apte <aj@snapser.com>"]
6
6
  readme = "README.md"
@@ -5,17 +5,18 @@ import base64
5
5
  from binascii import Error as BinasciiError
6
6
  import os
7
7
  import subprocess
8
+ import platform as sys_platform
8
9
  from sys import platform
9
10
  from typing import Union
10
11
 
11
- import typer
12
12
  from rich.progress import Progress, SpinnerColumn, TextColumn
13
13
  from snapctl.config.constants import SNAPCTL_BYOGS_DEPENDENCY_MISSING, \
14
14
  SNAPCTL_BYOGS_ECR_LOGIN_ERROR, SNAPCTL_BYOGS_BUILD_ERROR, \
15
15
  SNAPCTL_BYOGS_TAG_ERROR, SNAPCTL_BYOGS_PUBLISH_ERROR, \
16
16
  SNAPCTL_BYOGS_PUBLISH_DUPLICATE_TAG_ERROR, SNAPCTL_INPUT_ERROR
17
- from snapctl.utils.echo import error, success
18
- from snapctl.utils.helper import get_composite_token, snapctl_error, snapctl_success
17
+ from snapctl.utils.helper import get_composite_token, snapctl_error, snapctl_success, \
18
+ check_dockerfile_architecture
19
+ from snapctl.utils.echo import info, warning
19
20
 
20
21
 
21
22
  class ByoGs:
@@ -33,6 +34,7 @@ class ByoGs:
33
34
  def __init__(
34
35
  self, subcommand: str, base_url: str, api_key: str | None,
35
36
  input_tag: Union[str, None], path: Union[str, None], dockerfile: str,
37
+ skip_build: bool = False
36
38
  ) -> None:
37
39
  self.subcommand: str = subcommand
38
40
  self.base_url: str = base_url
@@ -49,6 +51,7 @@ class ByoGs:
49
51
  self.input_tag: Union[str, None] = input_tag
50
52
  self.path: Union[str, None] = path
51
53
  self.dockerfile: str = dockerfile
54
+ self.skip_build: bool = skip_build
52
55
  # Validate input
53
56
  self.validate_input()
54
57
 
@@ -143,23 +146,31 @@ class ByoGs:
143
146
  progress.add_task(
144
147
  description='Building your snap...', total=None)
145
148
  try:
146
- image_tag = f'{ByoGs.SID}.{self.input_tag}'
149
+ # image_tag = f'{ByoGs.SID}.{self.input_tag}'
147
150
  build_platform = ByoGs.DEFAULT_BUILD_PLATFORM
148
151
  if len(self.token_parts) == 4:
149
152
  build_platform = self.token_parts[3]
150
153
  # Build your snap
151
154
  docker_file_path = os.path.join(self.path, self.dockerfile)
155
+ # Warning check for architecture specific commands
156
+ info(f'Building on system architecture {sys_platform.machine()}')
157
+ check_response = check_dockerfile_architecture(
158
+ docker_file_path, sys_platform.machine())
159
+ if check_response['error']:
160
+ warning(check_response['message'])
161
+ # Build the image
152
162
  if platform == "win32":
153
163
  response = subprocess.run([
154
164
  # f"docker build --no-cache -t {tag} {path}"
155
- 'docker', 'build', '--platform', build_platform, '-t', image_tag,
165
+ 'docker', 'build', '--platform', build_platform, '-t', self.input_tag,
156
166
  '-f', docker_file_path, self.path
157
167
  ], shell=True, check=False)
158
168
  # stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
159
169
  else:
160
170
  response = subprocess.run([
161
171
  # f"docker build --no-cache -t {tag} {path}"
162
- f"docker build --platform {build_platform} -t {image_tag} "
172
+ f"docker build --platform {
173
+ build_platform} -t {self.input_tag} "
163
174
  f"-f {docker_file_path} {self.path}"
164
175
  ], shell=True, check=False)
165
176
  # stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
@@ -188,11 +199,11 @@ class ByoGs:
188
199
  # Tag the repo
189
200
  if platform == "win32":
190
201
  response = subprocess.run([
191
- 'docker', 'tag', image_tag, full_ecr_repo_url
202
+ 'docker', 'tag', self.input_tag, full_ecr_repo_url
192
203
  ], shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, check=False)
193
204
  else:
194
205
  response = subprocess.run([
195
- f"docker tag {image_tag} {full_ecr_repo_url}"
206
+ f"docker tag {self.input_tag} {full_ecr_repo_url}"
196
207
  ], shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, check=False)
197
208
  if not response.returncode:
198
209
  return snapctl_success('BYOGS tag successful', progress, no_exit=True)
@@ -246,7 +257,8 @@ class ByoGs:
246
257
  # Check subcommand
247
258
  if not self.subcommand in ByoGs.SUBCOMMANDS:
248
259
  snapctl_error(
249
- f"Invalid command. Valid commands are {', '.join(ByoGs.SUBCOMMANDS)}.",
260
+ f"Invalid command. Valid commands are {
261
+ ', '.join(ByoGs.SUBCOMMANDS)}.",
250
262
  SNAPCTL_INPUT_ERROR)
251
263
  # Validation for subcommands
252
264
  if self.token_parts is None:
@@ -263,11 +275,11 @@ class ByoGs:
263
275
  SNAPCTL_INPUT_ERROR
264
276
  )
265
277
  if self.subcommand in ['build', 'publish']:
266
- if not self.path:
278
+ if not self.skip_build and not self.path:
267
279
  snapctl_error("Missing required parameter: path",
268
280
  SNAPCTL_INPUT_ERROR)
269
281
  # Check path
270
- if not os.path.isfile(f"{self.path}/{self.dockerfile}"):
282
+ if not self.skip_build and not os.path.isfile(f"{self.path}/{self.dockerfile}"):
271
283
  snapctl_error(
272
284
  f"Unable to find {self.dockerfile} at path {self.path}", SNAPCTL_INPUT_ERROR)
273
285
  # elif self.subcommand == 'push':
@@ -313,7 +325,10 @@ class ByoGs:
313
325
  """
314
326
  self._check_dependencies()
315
327
  self._docker_login()
316
- self._docker_build()
328
+ if not self.skip_build:
329
+ self._docker_build()
330
+ else:
331
+ info('--skip-build set. Skipping the build step.')
317
332
  self._docker_tag()
318
333
  self._docker_push()
319
334
  snapctl_success('BYOGS publish successful')
@@ -7,6 +7,7 @@ import json
7
7
  import os
8
8
  import re
9
9
  import subprocess
10
+ import platform as sys_platform
10
11
  from sys import platform
11
12
  from typing import Union
12
13
  import requests
@@ -22,8 +23,9 @@ from snapctl.config.constants import HTTP_ERROR_SERVICE_VERSION_EXISTS, \
22
23
  SNAPCTL_BYOSNAP_CREATE_DUPLICATE_NAME_ERROR, SNAPCTL_BYOSNAP_CREATE_PERMISSION_ERROR, \
23
24
  SNAPCTL_BYOSNAP_CREATE_ERROR, SNAPCTL_BYOSNAP_PUBLISH_VERSION_DUPLICATE_TAG_ERROR, \
24
25
  SNAPCTL_BYOSNAP_PUBLISH_VERSION_ERROR
25
- from snapctl.utils.echo import success, info
26
- from snapctl.utils.helper import get_composite_token, snapctl_error, snapctl_success
26
+ from snapctl.utils.echo import success, info, warning
27
+ from snapctl.utils.helper import get_composite_token, snapctl_error, snapctl_success, \
28
+ check_dockerfile_architecture
27
29
 
28
30
 
29
31
  class ByoSnap:
@@ -41,12 +43,16 @@ class ByoSnap:
41
43
  TAG_CHARACTER_LIMIT = 80
42
44
  VALID_CPU_MARKS = [100, 250, 500, 750, 1000, 1500, 2000, 3000]
43
45
  VALID_MEMORY_MARKS = [0.125, 0.25, 0.5, 1, 2, 3, 4]
46
+ MAX_READINESS_TIMEOUT = 30
47
+ MAX_MIN_REPLICAS = 4
44
48
 
45
49
  def __init__(
46
50
  self, subcommand: str, base_url: str, api_key: str | None, sid: str, name: str,
47
51
  desc: str, platform_type: str, language: str, input_tag: Union[str, None],
48
52
  path: Union[str, None], dockerfile: str, prefix: str, version: Union[str, None],
49
- http_port: Union[int, None], byosnap_profile: Union[str, None]
53
+ http_port: Union[int, None], byosnap_profile: Union[str, None],
54
+ skip_build: bool = False, readiness_path: Union[str, None] = None,
55
+ readiness_delay: Union[int, None] = None
50
56
  ) -> None:
51
57
  self.subcommand: str = subcommand
52
58
  self.base_url: str = base_url
@@ -72,6 +78,9 @@ class ByoSnap:
72
78
  self.version: Union[str, None] = version
73
79
  self.http_port: Union[int, None] = http_port
74
80
  self.byosnap_profile: Union[str, None] = byosnap_profile
81
+ self.skip_build: bool = skip_build
82
+ self.readiness_path: Union[str, None] = readiness_path
83
+ self.readiness_delay: Union[int, None] = readiness_delay
75
84
  # Validate the input
76
85
  self.validate_input()
77
86
 
@@ -162,7 +171,7 @@ class ByoSnap:
162
171
 
163
172
  def _docker_build(self) -> None:
164
173
  # Get the data
165
- image_tag = f'{self.sid}.{self.input_tag}'
174
+ # image_tag = f'{self.sid}.{self.input_tag}'
166
175
  build_platform = ByoSnap.DEFAULT_BUILD_PLATFORM
167
176
  if len(self.token_parts) == 4:
168
177
  build_platform = self.token_parts[3]
@@ -177,17 +186,25 @@ class ByoSnap:
177
186
  try:
178
187
  # Build your snap
179
188
  docker_file_path = os.path.join(self.path, self.dockerfile)
189
+ # Warning check for architecture specific commands
190
+ info(f'Building on system architecture {sys_platform.machine()}')
191
+ check_response = check_dockerfile_architecture(
192
+ docker_file_path, sys_platform.machine())
193
+ if check_response['error']:
194
+ warning(check_response['message'])
195
+ # Build the image
180
196
  if platform == "win32":
181
197
  response = subprocess.run([
182
198
  # f"docker build --no-cache -t {tag} {path}"
183
- 'docker', 'build', '--platform', build_platform, '-t', image_tag,
199
+ 'docker', 'build', '--platform', build_platform, '-t', self.input_tag,
184
200
  '-f', docker_file_path, self.path
185
201
  ], shell=True, check=False)
186
202
  # stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
187
203
  else:
188
204
  response = subprocess.run([
189
205
  # f"docker build --no-cache -t {tag} {path}"
190
- f"docker build --platform {build_platform} -t {image_tag} "
206
+ f"docker build --platform {
207
+ build_platform} -t {self.input_tag} "
191
208
  f"-f {docker_file_path} {self.path}"
192
209
  ], shell=True, check=False)
193
210
  # stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
@@ -217,11 +234,11 @@ class ByoSnap:
217
234
  # Tag the repo
218
235
  if platform == "win32":
219
236
  response = subprocess.run([
220
- 'docker', 'tag', image_tag, full_ecr_repo_url
237
+ 'docker', 'tag', self.input_tag, full_ecr_repo_url
221
238
  ], shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, check=False)
222
239
  else:
223
240
  response = subprocess.run([
224
- f"docker tag {image_tag} {full_ecr_repo_url}"
241
+ f"docker tag {self.input_tag} {full_ecr_repo_url}"
225
242
  ], shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, check=False)
226
243
  if not response.returncode:
227
244
  return snapctl_success('BYOSnap tag successful',
@@ -298,6 +315,7 @@ class ByoSnap:
298
315
  SNAPCTL_INPUT_ERROR
299
316
  )
300
317
  for profile in ['dev_template', 'stage_template', 'prod_template']:
318
+ # Currently, not checking for 'min_replicas' not in profile_data[profile]
301
319
  if 'cpu' not in profile_data[profile] or \
302
320
  'memory' not in profile_data[profile] or \
303
321
  'cmd' not in profile_data[profile] or \
@@ -310,13 +328,25 @@ class ByoSnap:
310
328
  if profile_data[profile]['cpu'] not in ByoSnap.VALID_CPU_MARKS:
311
329
  snapctl_error(
312
330
  'Invalid CPU value in BYOSnap profile. '
313
- f'Valid values are {", ".join(map(str, ByoSnap.VALID_CPU_MARKS))}',
331
+ f'Valid values are {
332
+ ", ".join(map(str, ByoSnap.VALID_CPU_MARKS))}',
314
333
  SNAPCTL_INPUT_ERROR
315
334
  )
316
335
  if profile_data[profile]['memory'] not in ByoSnap.VALID_MEMORY_MARKS:
317
336
  snapctl_error(
318
337
  'Invalid Memory value in BYOSnap profile. '
319
- f'Valid values are {", ".join(map(str, ByoSnap.VALID_MEMORY_MARKS))}',
338
+ f'Valid values are {
339
+ ", ".join(map(str, ByoSnap.VALID_MEMORY_MARKS))}',
340
+ SNAPCTL_INPUT_ERROR
341
+ )
342
+ if 'min_replicas' in profile_data[profile] and \
343
+ (not isinstance(profile_data[profile]['min_replicas'], int) or
344
+ int(profile_data[profile]['min_replicas']) < 0 or
345
+ int(profile_data[profile]['min_replicas']) > ByoSnap.MAX_MIN_REPLICAS):
346
+ snapctl_error(
347
+ 'Invalid Min Replicas value in BYOSnap profile. '
348
+ f'Minimum replicas should be between 0 and {
349
+ ByoSnap.MAX_MIN_REPLICAS}',
320
350
  SNAPCTL_INPUT_ERROR
321
351
  )
322
352
 
@@ -340,13 +370,15 @@ class ByoSnap:
340
370
  # Validate the SID
341
371
  if not self.sid.startswith(ByoSnap.ID_PREFIX):
342
372
  snapctl_error(
343
- f"Invalid Snap ID. Valid Snap IDs start with {ByoSnap.ID_PREFIX}.",
373
+ f"Invalid Snap ID. Valid Snap IDs start with {
374
+ ByoSnap.ID_PREFIX}.",
344
375
  SNAPCTL_INPUT_ERROR
345
376
  )
346
377
  if len(self.sid) > ByoSnap.SID_CHARACTER_LIMIT:
347
378
  snapctl_error(
348
379
  "Invalid Snap ID. "
349
- f"Snap ID should be less than {ByoSnap.SID_CHARACTER_LIMIT} characters",
380
+ f"Snap ID should be less than {
381
+ ByoSnap.SID_CHARACTER_LIMIT} characters",
350
382
  SNAPCTL_INPUT_ERROR
351
383
  )
352
384
  # Validation for subcommands
@@ -372,7 +404,7 @@ class ByoSnap:
372
404
  if self.token_parts is None:
373
405
  snapctl_error('Invalid token. Please reach out to your support team.',
374
406
  SNAPCTL_INPUT_ERROR)
375
- if self.subcommand in ['build', 'publish-image']:
407
+ if self.subcommand in ['publish-image']:
376
408
  if not self.input_tag:
377
409
  snapctl_error(
378
410
  "Missing required parameter: tag", SNAPCTL_INPUT_ERROR)
@@ -383,13 +415,14 @@ class ByoSnap:
383
415
  f"{ByoSnap.TAG_CHARACTER_LIMIT} characters",
384
416
  SNAPCTL_INPUT_ERROR
385
417
  )
386
- if not self.path:
418
+ if not self.skip_build and not self.path:
387
419
  snapctl_error("Missing required parameter: path",
388
420
  SNAPCTL_INPUT_ERROR)
389
421
  # Check path
390
- if not os.path.isfile(f"{self.path}/{self.dockerfile}"):
422
+ if not self.skip_build and not os.path.isfile(f"{self.path}/{self.dockerfile}"):
391
423
  snapctl_error(
392
- f"Unable to find {self.dockerfile} at path {self.path}",
424
+ f"Unable to find {
425
+ self.dockerfile} at path {self.path}",
393
426
  SNAPCTL_INPUT_ERROR)
394
427
  # elif self.subcommand == 'push':
395
428
  # if not self.input_tag:
@@ -403,7 +436,8 @@ class ByoSnap:
403
436
  if not self.input_tag:
404
437
  snapctl_error(
405
438
  "Missing required parameter: tag", SNAPCTL_INPUT_ERROR)
406
- if len(self.input_tag.split()) > 1 or len(self.input_tag) > ByoSnap.TAG_CHARACTER_LIMIT:
439
+ if len(self.input_tag.split()) > 1 or \
440
+ len(self.input_tag) > ByoSnap.TAG_CHARACTER_LIMIT:
407
441
  snapctl_error(
408
442
  "Tag should be a single word with maximum of "
409
443
  f"{ByoSnap.TAG_CHARACTER_LIMIT} characters",
@@ -429,6 +463,18 @@ class ByoSnap:
429
463
  if not self.http_port.isdigit():
430
464
  snapctl_error("Ingress HTTP Port should be a number",
431
465
  SNAPCTL_INPUT_ERROR)
466
+ if self.readiness_path is not None:
467
+ if self.readiness_path.strip() == '':
468
+ snapctl_error("Readiness path cannot be empty",
469
+ SNAPCTL_INPUT_ERROR)
470
+ if not self.readiness_path.strip().startswith('/'):
471
+ snapctl_error("Readiness path has to start with /",
472
+ SNAPCTL_INPUT_ERROR)
473
+ if self.readiness_delay is not None:
474
+ if self.readiness_delay < 0 or \
475
+ self.readiness_delay > ByoSnap.MAX_READINESS_TIMEOUT:
476
+ snapctl_error("Readiness delay should be between 0 "
477
+ f"and {ByoSnap.MAX_READINESS_TIMEOUT}", SNAPCTL_INPUT_ERROR)
432
478
  # Check byosnap_profile path
433
479
  self._validate_byosnap_profile()
434
480
 
@@ -493,7 +539,8 @@ class ByoSnap:
493
539
  info('Unable to upload your swagger.json')
494
540
  except RequestException as e:
495
541
  info(
496
- f'Exception: Unable to find swagger.json at {self.path} {e}'
542
+ f'Exception: Unable to find swagger.json at {
543
+ self.path} {e}'
497
544
  )
498
545
  else:
499
546
  info(f'No swagger.json found at {self.path}'
@@ -559,7 +606,8 @@ class ByoSnap:
559
606
  if "api_error_code" in response_json and "message" in response_json:
560
607
  if response_json['api_error_code'] == HTTP_ERROR_SERVICE_VERSION_EXISTS:
561
608
  snapctl_error(
562
- f'BYOSnap {self.name} already exists. Please use a different name',
609
+ f'BYOSnap {
610
+ self.name} already exists. Please use a different name',
563
611
  SNAPCTL_BYOSNAP_CREATE_DUPLICATE_NAME_ERROR,
564
612
  progress
565
613
  )
@@ -592,10 +640,14 @@ class ByoSnap:
592
640
  """
593
641
  self._check_dependencies()
594
642
  self._docker_login()
595
- self._docker_build()
643
+ if not self.skip_build:
644
+ self._docker_build()
645
+ else:
646
+ info('--skip-build set. Skipping the build step.')
596
647
  self._docker_tag()
597
648
  self._docker_push()
598
- self.upload_docs()
649
+ if self.path is not None:
650
+ self.upload_docs()
599
651
  snapctl_success('BYOSNAP publish successful')
600
652
 
601
653
  def publish_version(self) -> None:
@@ -622,6 +674,10 @@ class ByoSnap:
622
674
  "image_tag": self.input_tag,
623
675
  "base_url": f"{self.prefix}/{self.sid}",
624
676
  "http_port": self.http_port,
677
+ "readiness_probe_config": {
678
+ "path": self.readiness_path,
679
+ "initial_delay_seconds": self.readiness_delay
680
+ },
625
681
  "dev_template": profile_data['dev_template'],
626
682
  "stage_template": profile_data['stage_template'],
627
683
  "prod_template": profile_data['prod_template']
@@ -0,0 +1,215 @@
1
+ """
2
+ Generate CLI commands
3
+ """
4
+ import base64
5
+ from binascii import Error as BinasciiError
6
+ import json
7
+ import os
8
+ from typing import Union
9
+ from rich.progress import Progress, SpinnerColumn, TextColumn
10
+ from snapctl.config.constants import SNAPCTL_INPUT_ERROR, \
11
+ SNAPCTL_GENERATE_GENERIC_ERROR, SNAPCTL_GENERATE_PROFILE_ERROR, \
12
+ SNAPCTL_GENERATE_CREDENTIALS_ERROR
13
+ from snapctl.config.hashes import BYOSNAP_TEMPLATE
14
+ from snapctl.utils.helper import get_composite_token, snapctl_error, snapctl_success
15
+
16
+
17
+ class Generate:
18
+ """
19
+ Generate CLI commands
20
+ """
21
+ SUBCOMMANDS = ['byosnap-profile', 'profile', 'credentials']
22
+ DEPRECATED_SOON_SUBCOMMANDS = ['byosnap-profile']
23
+ BYOSNAP_PROFILE_FN = 'snapser-byosnap-profile.json'
24
+ ECR_TOKEN_FN = 'snapser-ecr-credentials.json'
25
+ CATEGORIES = {
26
+ 'profile': ['byosnap'],
27
+ 'credentials': ['ecr']
28
+ }
29
+
30
+ def __init__(
31
+ self, subcommand: str, base_url: str, api_key: str | None,
32
+ category: str | None,
33
+ out_path: Union[str, None]
34
+ ) -> None:
35
+ self.subcommand: str = subcommand
36
+ self.base_url: str = base_url
37
+ self.api_key: str = api_key
38
+ self.category: str | None = category
39
+ self.out_path: Union[str, None] = out_path
40
+ # Validate input
41
+ self.validate_input()
42
+
43
+ # Private methods
44
+ @staticmethod
45
+ def _get_token_values(token: str) -> None | list:
46
+ """
47
+ Get the token values
48
+ """
49
+ try:
50
+ input_token = base64.b64decode(token).decode('ascii')
51
+ token_parts = input_token.split('|')
52
+ # url|web_app_token|service_id|ecr_repo_url|ecr_repo_username|ecr_repo_token
53
+ if len(token_parts) >= 3:
54
+ return token_parts
55
+ except BinasciiError:
56
+ pass
57
+ return None
58
+ # Validator
59
+
60
+ def validate_input(self) -> None:
61
+ """
62
+ Validator
63
+ """
64
+ if self.subcommand not in Generate.SUBCOMMANDS:
65
+ snapctl_error(
66
+ f"Invalid command {self.subcommand}. Valid command are "
67
+ f"{Generate.SUBCOMMANDS}",
68
+ SNAPCTL_INPUT_ERROR
69
+ )
70
+ # Check path
71
+ if self.subcommand == 'profile':
72
+ if self.category not in Generate.CATEGORIES['profile']:
73
+ snapctl_error(
74
+ f"Invalid category {self.category}. Valid category are "
75
+ f"{Generate.CATEGORIES['profile']}",
76
+ SNAPCTL_INPUT_ERROR
77
+ )
78
+ if not self.out_path:
79
+ snapctl_error(
80
+ "Path is required for profile generation",
81
+ SNAPCTL_INPUT_ERROR
82
+ )
83
+ elif self.subcommand == 'credentials':
84
+ if self.category not in Generate.CATEGORIES['credentials']:
85
+ snapctl_error(
86
+ f"Invalid category {self.category}. Valid category are "
87
+ f"{Generate.CATEGORIES['credentials']}",
88
+ SNAPCTL_INPUT_ERROR
89
+ )
90
+ if not self.out_path:
91
+ snapctl_error(
92
+ "Path is required for token generation",
93
+ SNAPCTL_INPUT_ERROR
94
+ )
95
+ # Now confirm that out-path is valid
96
+ if self.out_path and not os.path.isdir(self.out_path):
97
+ snapctl_error(
98
+ f"Invalid path {self.out_path}. Wont be able to "
99
+ "store the output file",
100
+ SNAPCTL_INPUT_ERROR
101
+ )
102
+
103
+ def byosnap_profile(self) -> None:
104
+ """
105
+ Generate snapser-byosnap-profile.json
106
+ """
107
+ progress = Progress(
108
+ SpinnerColumn(),
109
+ TextColumn("[progress.description]{task.description}"),
110
+ transient=True,
111
+ )
112
+ progress.start()
113
+ progress.add_task(
114
+ description='Generating BYOSnap profile...', total=None)
115
+ try:
116
+ if self.out_path is not None:
117
+ file_save_path = os.path.join(
118
+ self.out_path, Generate.BYOSNAP_PROFILE_FN)
119
+ else:
120
+ file_save_path = os.path.join(
121
+ os.getcwd(), Generate.BYOSNAP_PROFILE_FN)
122
+ file_written = False
123
+ with open(file_save_path, "w") as file:
124
+ json.dump(BYOSNAP_TEMPLATE, file, indent=4)
125
+ file_written = True
126
+ if file_written:
127
+ snapctl_success(
128
+ "BYOSNAP Profile generation successful. "
129
+ f"{Generate.BYOSNAP_PROFILE_FN} saved at {file_save_path}",
130
+ progress
131
+ )
132
+ except (IOError, OSError) as file_error:
133
+ snapctl_error(f"File error: {file_error}",
134
+ SNAPCTL_GENERATE_PROFILE_ERROR, progress)
135
+ except json.JSONDecodeError as json_error:
136
+ snapctl_error(f"JSON error: {json_error}",
137
+ SNAPCTL_GENERATE_PROFILE_ERROR, progress)
138
+ snapctl_error(
139
+ "Failed to generate BYOSNAP Profile",
140
+ SNAPCTL_GENERATE_PROFILE_ERROR,
141
+ progress
142
+ )
143
+
144
+ def profile(self) -> None:
145
+ """
146
+ Generate profile
147
+ """
148
+ if self.category == 'byosnap':
149
+ self.byosnap_profile()
150
+
151
+ def ecr_credentials(self) -> None:
152
+ """
153
+ Generate credentials
154
+ """
155
+
156
+ progress = Progress(
157
+ SpinnerColumn(),
158
+ TextColumn("[progress.description]{task.description}"),
159
+ transient=True,
160
+ )
161
+ progress.start()
162
+ progress.add_task(
163
+ description='Generating ECR credentials...', total=None)
164
+ try:
165
+ composite_token: Union[str, None] = get_composite_token(
166
+ self.base_url, self.api_key, 'byogs', {'service_id': 'byogs'}
167
+ )
168
+ token_parts = Generate._get_token_values(composite_token)
169
+ # url|web_app_token|service_id|ecr_repo_url|ecr_repo_username|ecr_repo_token
170
+ if token_parts is None or len(token_parts) != 4:
171
+ snapctl_error(
172
+ "Unable to retrieve token.",
173
+ SNAPCTL_GENERATE_GENERIC_ERROR,
174
+ progress
175
+ )
176
+ token_details = {
177
+ 'ecr_repo_url': token_parts[0],
178
+ 'ecr_repo_username': token_parts[1],
179
+ 'ecr_repo_token': token_parts[2],
180
+ 'ecr_repo_platform': token_parts[3]
181
+ }
182
+ if self.out_path is not None:
183
+ file_save_path = os.path.join(
184
+ self.out_path, Generate.ECR_TOKEN_FN)
185
+ else:
186
+ file_save_path = os.path.join(
187
+ os.getcwd(), Generate.ECR_TOKEN_FN)
188
+ file_written = False
189
+ with open(file_save_path, "w") as file:
190
+ json.dump(token_details, file, indent=4)
191
+ file_written = True
192
+ if file_written:
193
+ snapctl_success(
194
+ "ECR Token generation successful. "
195
+ f"{Generate.ECR_TOKEN_FN} saved at {file_save_path}",
196
+ progress
197
+ )
198
+ except (IOError, OSError) as file_error:
199
+ snapctl_error(f"File error: {file_error}",
200
+ SNAPCTL_GENERATE_CREDENTIALS_ERROR, progress)
201
+ except json.JSONDecodeError as json_error:
202
+ snapctl_error(f"JSON error: {json_error}",
203
+ SNAPCTL_GENERATE_CREDENTIALS_ERROR, progress)
204
+ snapctl_error(
205
+ "Failed to generate Token",
206
+ SNAPCTL_GENERATE_CREDENTIALS_ERROR,
207
+ progress
208
+ )
209
+
210
+ def credentials(self):
211
+ """
212
+ Generate credentials
213
+ """
214
+ if self.category == 'ecr':
215
+ self.ecr_credentials()
@@ -2,7 +2,7 @@
2
2
  Constants used by snapctl
3
3
  """
4
4
  COMPANY_NAME = 'Snapser'
5
- VERSION = '0.32.2'
5
+ VERSION = '0.38.0'
6
6
  CONFIG_FILE_MAC = '~/.snapser/config'
7
7
  CONFIG_FILE_WIN = '%homepath%\\.snapser\\config'
8
8
 
@@ -88,4 +88,5 @@ SNAPCTL_SNAPEND_STATE_ERROR = 75
88
88
 
89
89
  # Generate Errors
90
90
  SNAPCTL_GENERATE_GENERIC_ERROR = 80
91
- SNAPCTL_GENERATE_BYOSNAP_PROFILE_ERROR = 81
91
+ SNAPCTL_GENERATE_PROFILE_ERROR = 81
92
+ SNAPCTL_GENERATE_CREDENTIALS_ERROR = 82
@@ -149,23 +149,49 @@ SNAPEND_MANIFEST_TYPES: dict[str, dict[str, str]] = {
149
149
  },
150
150
  }
151
151
 
152
- SERVICE_IDS = [
152
+ SERVICE_IDS: list[str] = [
153
153
  'analytics', 'auth', 'client-logs', 'events', 'experiments', 'gdpr', 'guilds', 'hades', 'iap',
154
154
  'inventory', 'leaderboards', 'matchmaking', 'notifications', 'parties', 'profiles', 'quests',
155
155
  'relay', 'remote-config', 'scheduler', 'sequencer', 'social-graph', 'statistics', 'storage',
156
156
  'trackables', 'xp'
157
157
  ]
158
158
 
159
- DEFAULT_BYOSNAP_TEMPLATE = {
159
+ DEFAULT_BYOSNAP_DEV_TEMPLATE = {
160
160
  'cpu': 100,
161
161
  'memory': 0.125,
162
+ 'min_replicas': 1,
162
163
  'cmd': '',
163
164
  'args': [],
164
- 'env_params': []
165
+ 'env_params': [{'key': "SNAPSER_ENVIRONMENT", 'value': "DEVELOPMENT"}]
166
+ }
167
+
168
+ DEFAULT_BYOSNAP_STAGE_TEMPLATE = {
169
+ 'cpu': 100,
170
+ 'memory': 0.125,
171
+ 'min_replicas': 1,
172
+ 'cmd': '',
173
+ 'args': [],
174
+ 'env_params': [{'key': "SNAPSER_ENVIRONMENT", 'value': "STAGING"}]
175
+ }
176
+
177
+ DEFAULT_BYOSNAP_PROD_TEMPLATE = {
178
+ 'cpu': 100,
179
+ 'memory': 0.125,
180
+ 'min_replicas': 2,
181
+ 'cmd': '',
182
+ 'args': [],
183
+ 'env_params': [{'key': "SNAPSER_ENVIRONMENT", 'value': "PRODUCTION"}]
165
184
  }
166
185
 
167
186
  BYOSNAP_TEMPLATE = {
168
- 'dev_template': DEFAULT_BYOSNAP_TEMPLATE,
169
- 'stage_template': DEFAULT_BYOSNAP_TEMPLATE,
170
- 'prod_template': DEFAULT_BYOSNAP_TEMPLATE
187
+ 'dev_template': DEFAULT_BYOSNAP_DEV_TEMPLATE,
188
+ 'stage_template': DEFAULT_BYOSNAP_STAGE_TEMPLATE,
189
+ 'prod_template': DEFAULT_BYOSNAP_PROD_TEMPLATE
190
+ }
191
+
192
+ ARCHITECTURE_MAPPING: dict[str, str] = {
193
+ 'x86_64': 'amd64',
194
+ 'arm64': 'arm64',
195
+ 'aarch64': 'arm64',
196
+ 'amd64': 'amd64'
171
197
  }
@@ -76,7 +76,8 @@ def extract_config(extract_key: str, profile: str | None = None) -> object:
76
76
  result['location'] = f'"{config_file_path}:profile {profile}"'
77
77
  config_profile = f'profile {profile}'
78
78
  info(
79
- f'Trying to extract API KEY from "{config_file_path}:profile {profile}"'
79
+ f'Trying to extract API KEY from "{
80
+ config_file_path}:profile {profile}"'
80
81
  )
81
82
  result['value'] = config.get(
82
83
  config_profile, extract_key, fallback=None, raw=True
@@ -203,7 +204,7 @@ def version_callback(value: bool = True):
203
204
  def common(
204
205
  ctx: typer.Context,
205
206
  version: bool = typer.Option(
206
- None, "--version",
207
+ None, "--version", "-v",
207
208
  help="Get the Snapctl version.",
208
209
  callback=version_callback
209
210
  ),
@@ -257,6 +258,9 @@ def byogs(
257
258
  docker_file: str = typer.Option(
258
259
  "Dockerfile", help="Dockerfile name to use"
259
260
  ),
261
+ skip_build: bool = typer.Option(
262
+ False, "--skip-build", help="(optional: publish) Skip the build step. You have to pass the image tag you used during the build step."
263
+ ),
260
264
  # overrides
261
265
  api_key: Union[str, None] = typer.Option(
262
266
  None, "--api-key", help="API Key override.", callback=api_key_context_callback
@@ -271,7 +275,7 @@ def byogs(
271
275
  validate_command_context(ctx)
272
276
  byogs_obj: ByoGs = ByoGs(
273
277
  subcommand, ctx.obj['base_url'], ctx.obj['api_key'],
274
- tag, path, docker_file,
278
+ tag, path, docker_file, skip_build
275
279
  )
276
280
  getattr(byogs_obj, subcommand.replace('-', '_'))()
277
281
  success(f"BYOGs {subcommand} complete")
@@ -306,12 +310,12 @@ def byosnap(
306
310
  # publish-image and publish-version
307
311
  tag: str = typer.Option(
308
312
  None, "--tag", help=(
309
- "(req: build, push publish-image and publish-version) Tag for your snap"
313
+ "(req: publish-image and publish-version) Tag for your snap"
310
314
  )
311
315
  ),
312
316
  # publish-image
313
317
  path: Union[str, None] = typer.Option(
314
- None, "--path", help="(req: build, publish-image) Path to your snap code"
318
+ None, "--path", help="(req: publish-image) Path to your snap code"
315
319
  ),
316
320
  docker_file: str = typer.Option(
317
321
  "Dockerfile", help="Dockerfile name to use"
@@ -329,9 +333,18 @@ def byosnap(
329
333
  ),
330
334
  byosnap_profile: Union[str, None] = typer.Option(
331
335
  None, "--byosnap-profile", help=(
332
- "(req: publish-version) Path to your byosnap-profile JSON file"
336
+ "(req: publish-version) Path to your byosnap-profile JSON file. You can generate a base version of this file using the `snapctl generate profile --category byosnap --out-path <output_path>` command."
333
337
  )
334
338
  ),
339
+ skip_build: bool = typer.Option(
340
+ False, "--skip-build", help="(optional: publish-image) Skip the build step. You have to pass the image tag you used during the build step."
341
+ ),
342
+ readiness_path: str = typer.Option(
343
+ None, "--readiness-path", help="(req: publish-version) Readiness path for your snap"
344
+ ),
345
+ readiness_delay: int = typer.Option(
346
+ None, "--readiness-delay", help="(req: publish-version) Delay before readiness check"
347
+ ),
335
348
  # overrides
336
349
  api_key: Union[str, None] = typer.Option(
337
350
  None, "--api-key", help="API Key override.", callback=api_key_context_callback
@@ -347,7 +360,8 @@ def byosnap(
347
360
  byosnap_obj: ByoSnap = ByoSnap(
348
361
  subcommand, ctx.obj['base_url'], ctx.obj['api_key'], sid,
349
362
  name, desc, platform_type, language, tag, path, docker_file,
350
- prefix, version, http_port, byosnap_profile
363
+ prefix, version, http_port, byosnap_profile, skip_build,
364
+ readiness_path, readiness_delay
351
365
  )
352
366
  getattr(byosnap_obj, subcommand.replace('-', '_'))()
353
367
  success(f"BYOSnap {subcommand} complete")
@@ -390,11 +404,28 @@ def generate(
390
404
  ctx: typer.Context,
391
405
  # Required fields
392
406
  subcommand: str = typer.Argument(
393
- ..., help="Generate Subcommands: " + ", ".join(Generate.SUBCOMMANDS) + "."
407
+ ..., help=(
408
+ "Generate Subcommands: " + \
409
+ ", ".join(Generate.SUBCOMMANDS) + "." + " "
410
+ "Deprecation Notice: " + \
411
+ ",".join(Generate.DEPRECATED_SOON_SUBCOMMANDS) + \
412
+ " will be deprecated soon. "
413
+ "Use `snapctl generate profile --category byosnap --out-path <output_path>` command instead."
414
+ )
394
415
  ),
395
- # byosnap-profile
416
+ category: Union[str, None] = typer.Option(
417
+ None, "--category",
418
+ help=(
419
+ "(req: profile, token) (profile: " +
420
+ ", ".join(Generate.CATEGORIES['profile']) +
421
+ ") (token: " + ", ".join(Generate.CATEGORIES['credentials']) + ')'
422
+ )
423
+ ),
424
+ # byosnap-profile, profile
396
425
  out_path: Union[str, None] = typer.Option(
397
- None, "--out-path", help="(req: byosnap-profile) Path to output the byosnap profile"
426
+ None, "--out-path", help=(
427
+ "(req: byosnap-profile, profile, token) Path to output the byosnap profile"
428
+ )
398
429
  ),
399
430
  # overrides
400
431
  api_key: Union[str, None] = typer.Option(
@@ -410,14 +441,14 @@ def generate(
410
441
  validate_command_context(ctx)
411
442
  generate_obj: Generate = Generate(
412
443
  subcommand, ctx.obj['base_url'], ctx.obj['api_key'],
413
- out_path,
444
+ category, out_path,
414
445
  )
415
446
  getattr(generate_obj, subcommand.replace('-', '_'))()
416
447
  success(f"Generate {subcommand} complete")
417
448
  raise typer.Exit(code=SNAPCTL_SUCCESS)
418
449
 
419
450
 
420
- @app.command()
451
+ @ app.command()
421
452
  def snapend(
422
453
  ctx: typer.Context,
423
454
  # Required fields
@@ -20,6 +20,13 @@ def error(msg: str, code: int = SNAPCTL_ERROR, data: object = None) -> None:
20
20
  typer.echo(json.dumps(error_response.to_dict()), err=True)
21
21
 
22
22
 
23
+ def warning(msg: str) -> None:
24
+ """
25
+ Prints a warning message to the console.
26
+ """
27
+ print(f"[bold yellow]Warning[/bold yellow] {msg}")
28
+
29
+
23
30
  def info(msg: str) -> None:
24
31
  """
25
32
  Prints an info message to the console.
@@ -1,16 +1,18 @@
1
1
  """
2
2
  Helper functions for snapctl
3
3
  """
4
+ import re
4
5
  import requests
5
6
  import typer
6
7
  from requests.exceptions import RequestException
7
8
  from rich.progress import Progress
8
9
  from snapctl.config.constants import HTTP_NOT_FOUND, HTTP_FORBIDDEN, HTTP_UNAUTHORIZED, \
9
10
  SERVER_CALL_TIMEOUT, SNAPCTL_CONFIGURATION_ERROR, SNAPCTL_SUCCESS
11
+ from snapctl.config.hashes import ARCHITECTURE_MAPPING
10
12
  from snapctl.utils.echo import error, success
11
13
 
12
14
 
13
- def validate_api_key(base_url: str, api_key: str | None):
15
+ def validate_api_key(base_url: str, api_key: str | None) -> bool:
14
16
  """
15
17
  This function validates the API Key
16
18
  """
@@ -74,6 +76,63 @@ def get_composite_token(base_url: str, api_key: str | None, action: str, params:
74
76
  return res.json()['token']
75
77
 
76
78
 
79
+ def check_dockerfile_architecture(dockerfile_path: str, system_arch: str) -> dict[str, object]:
80
+ """
81
+ Check the Dockerfile for architecture specific commands
82
+ """
83
+ response = {
84
+ 'error': False,
85
+ 'message': ''
86
+ }
87
+ # Normalize system architecture
88
+ system_arch = ARCHITECTURE_MAPPING.get(system_arch, system_arch)
89
+ try:
90
+ lines = []
91
+ with open(dockerfile_path, 'r') as file:
92
+ lines = file.readlines()
93
+ for line_number, line in enumerate(lines, 1):
94
+ if line.startswith('#'):
95
+ continue
96
+ # Checking various build and run commands for architecture specifics
97
+ patterns = [
98
+ # FROM with platform
99
+ r'FROM --platform=linux/(\w+)',
100
+ # dotnet runtime
101
+ r'-r linux-(\w+)',
102
+ # Build args specifying arch
103
+ r'--build-arg ARCH=(\w+)',
104
+ # Environment variables setting arch
105
+ r'ENV ARCH=(\w+)',
106
+ # cmake specifying arch
107
+ r'cmake.*?-DARCH=(\w+)',
108
+ # make specifying arch
109
+ r'make.*?ARCH=(\w+)'
110
+ ]
111
+
112
+ for pattern in patterns:
113
+ match = re.search(pattern, line)
114
+ if match and ARCHITECTURE_MAPPING.get(match.group(1)) != system_arch:
115
+ response['error'] = True
116
+ response['message'] = (
117
+ f'[Architecture Mismatch] Line {line_number}: "{
118
+ line.strip()}" '
119
+ f' of Dockerfile {dockerfile_path}'
120
+ f' specifies architecture {
121
+ match.group(1)}, which does not match the '
122
+ f' systems ({system_arch}).'
123
+ )
124
+ return response
125
+ except FileNotFoundError:
126
+ response['error'] = True
127
+ response['message'] = f'Dockerfile not found at {dockerfile_path}'
128
+ return response
129
+ except Exception as e:
130
+ response['error'] = True
131
+ response['message'] = f'Exception {e}'
132
+ return response
133
+ return response
134
+
135
+
77
136
  def snapctl_success(message: str, progress: Progress | None = None, no_exit: bool = False):
78
137
  """
79
138
  This function exits the snapctl
@@ -1,93 +0,0 @@
1
- """
2
- Generate CLI commands
3
- """
4
- import json
5
- import os
6
- from typing import Union
7
- from rich.progress import Progress, SpinnerColumn, TextColumn
8
- from snapctl.config.constants import SNAPCTL_INPUT_ERROR, \
9
- SNAPCTL_GENERATE_GENERIC_ERROR
10
- from snapctl.config.hashes import BYOSNAP_TEMPLATE
11
- from snapctl.utils.echo import error
12
- from snapctl.utils.helper import snapctl_error, snapctl_success
13
-
14
-
15
- class Generate:
16
- """
17
- Generate CLI commands
18
- """
19
- SUBCOMMANDS = ['byosnap-profile']
20
- BYOSNAP_PROFILE_FN = 'snapser-byosnap-profile.json'
21
-
22
- def __init__(
23
- self, subcommand: str, base_url: str, api_key: str | None,
24
- out_path: Union[str, None]
25
- ) -> None:
26
- self.subcommand: str = subcommand
27
- self.base_url: str = base_url
28
- self.api_key: str = api_key
29
- self.out_path: Union[str, None] = out_path
30
- # Validate input
31
- self.validate_input()
32
-
33
- # Validator
34
-
35
- def validate_input(self) -> None:
36
- """
37
- Validator
38
- """
39
- if self.subcommand not in Generate.SUBCOMMANDS:
40
- snapctl_error(
41
- f"Invalid command {self.subcommand}. Valid command are "
42
- f"{Generate.SUBCOMMANDS}",
43
- SNAPCTL_INPUT_ERROR
44
- )
45
- if self.subcommand in ['byosnap-profile']:
46
- # Check path
47
- if self.out_path and not os.path.isdir(self.out_path):
48
- snapctl_error(
49
- f"Invalid path {self.out_path}. Wont be able to "
50
- "store the byosnap-profile.json file",
51
- SNAPCTL_INPUT_ERROR
52
- )
53
-
54
- def byosnap_profile(self) -> None:
55
- """
56
- Generate snapser-byosnap-profile.json
57
- """
58
- progress = Progress(
59
- SpinnerColumn(),
60
- TextColumn("[progress.description]{task.description}"),
61
- transient=True,
62
- )
63
- progress.start()
64
- progress.add_task(
65
- description='Promoting your staging snapend...', total=None)
66
- try:
67
- if self.out_path is not None:
68
- file_save_path = os.path.join(
69
- self.out_path, Generate.BYOSNAP_PROFILE_FN)
70
- else:
71
- file_save_path = os.path.join(
72
- os.getcwd(), Generate.BYOSNAP_PROFILE_FN)
73
- file_written = False
74
- with open(file_save_path, "w") as file:
75
- json.dump(BYOSNAP_TEMPLATE, file, indent=4)
76
- file_written = True
77
- if file_written:
78
- snapctl_success(
79
- "BYOSNAP Profile generation successful. "
80
- f"{Generate.BYOSNAP_PROFILE_FN} saved at {file_save_path}",
81
- progress
82
- )
83
- except (IOError, OSError) as file_error:
84
- snapctl_error(f"File error: {file_error}",
85
- SNAPCTL_GENERATE_GENERIC_ERROR, progress)
86
- except json.JSONDecodeError as json_error:
87
- snapctl_error(f"JSON error: {json_error}",
88
- SNAPCTL_GENERATE_GENERIC_ERROR, progress)
89
- snapctl_error(
90
- "Failed to generate BYOSNAP Profile",
91
- SNAPCTL_GENERATE_GENERIC_ERROR,
92
- progress
93
- )
File without changes
File without changes
File without changes