snapctl 0.4.5__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.

@@ -1,364 +1,569 @@
1
+ """
2
+ BYOSnap CLI commands
3
+ """
1
4
  import base64
5
+ from binascii import Error as BinasciiError
2
6
  import json
3
7
  import os
4
8
  import re
5
- import requests
6
9
  import subprocess
7
-
8
- from rich.progress import Progress, SpinnerColumn, TextColumn
9
10
  from sys import platform
10
11
  from typing import Union
11
- from snapctl.config.constants import ERROR_SERVICE_VERSION_EXISTS, ERROR_TAG_NOT_AVAILABLE
12
+ import requests
13
+ from requests.exceptions import RequestException
14
+
15
+ from rich.progress import Progress, SpinnerColumn, TextColumn
16
+ from snapctl.config.constants import SERVER_CALL_TIMEOUT
17
+ from snapctl.config.constants import ERROR_SERVICE_VERSION_EXISTS, ERROR_TAG_NOT_AVAILABLE, \
18
+ ERROR_ADD_ON_NOT_ENABLED
12
19
  from snapctl.types.definitions import ResponseType
13
20
  from snapctl.utils.echo import error, success, info
14
21
  from snapctl.utils.helper import get_composite_token
15
22
 
16
- class ByoSnap:
17
- ID_PREFIX = 'byosnap-'
18
- SUBCOMMANDS = ['create', 'publish-image', 'publish-version', 'upload-docs']
19
- PLATFORMS = ['linux/arm64', 'linux/amd64']
20
- LANGUAGES = ['go', 'python', 'ruby', 'c#', 'c++', 'rust', 'java', 'node']
21
- DEFAULT_BUILD_PLATFORM = 'linux/arm64'
22
23
 
23
- def __init__(self, subcommand: str, base_url: str, api_key: str, sid: str, name: str, desc: str, platform: str, language: str, tag: Union[str, None], path: Union[str, None], dockerfile: str, prefix: str, version: Union[str, None], http_port: Union[int, None]) -> None:
24
- self.subcommand: str = subcommand
25
- self.base_url: str = base_url
26
- self.api_key: str = api_key
27
- self.sid: str = sid
28
- self.name: str = name
29
- self.desc: str = desc
30
- self.platform: str = platform
31
- self.language: str = language
32
- self.token: Union[str, None] = get_composite_token(base_url, api_key, 'byosnap', {'service_id': sid}) if subcommand != 'create' else None
33
- self.token_parts: Union[list, None] = ByoSnap.get_token_values(self.token) if self.token is not None else None
34
- self.tag: Union[str, None] = tag
35
- self.path: Union[str, None] = path
36
- self.dockerfile: str = dockerfile
37
- self.prefix: str = prefix
38
- self.version: Union[str, None] = version
39
- self.http_port: Union[int, None] = http_port
24
+ class ByoSnap:
25
+ """
26
+ CLI commands exposed for a BYOSnap
27
+ """
28
+ ID_PREFIX = 'byosnap-'
29
+ SUBCOMMANDS = [
30
+ 'build', 'push', 'upload-docs',
31
+ 'create', 'publish-image', 'publish-version',
32
+ ]
33
+ PLATFORMS = ['linux/arm64', 'linux/amd64']
34
+ LANGUAGES = ['go', 'python', 'ruby', 'c#', 'c++', 'rust', 'java', 'node']
35
+ DEFAULT_BUILD_PLATFORM = 'linux/arm64'
36
+ SID_CHARACTER_LIMIT = 47
37
+ TAG_CHARACTER_LIMIT = 80
40
38
 
41
- @staticmethod
42
- def get_token_values(token: str) -> None | list:
43
- try:
44
- input_token = base64.b64decode(token).decode('ascii')
45
- parts = input_token.split('|')
46
- # url|web_app_token|service_id|ecr_repo_url|ecr_repo_username|ecr_repo_token
47
- # url = self.token_parts[0]
48
- # web_app_token = self.token_parts[1]
49
- # service_id = self.token_parts[2]
50
- # ecr_repo_url = self.token_parts[3]
51
- # ecr_repo_username = self.token_parts[4]
52
- # ecr_repo_token = self.token_parts[5]
53
- # platform = self.token_parts[6]
54
- if len(parts) >= 3:
55
- return parts
56
- except Exception:
57
- pass
58
- return None
39
+ def __init__(
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],
42
+ path: Union[str, None], dockerfile: str, prefix: str, version: Union[str, None],
43
+ http_port: Union[int, None]
44
+ ) -> None:
45
+ self.subcommand: str = subcommand
46
+ self.base_url: str = base_url
47
+ self.api_key: str = api_key
48
+ self.sid: str = sid
49
+ self.name: str = name
50
+ self.desc: str = desc
51
+ self.platform_type: str = platform_type
52
+ self.language: str = language
53
+ if subcommand != 'create':
54
+ self.token: Union[str, None] = get_composite_token(
55
+ base_url, api_key,
56
+ 'byosnap', {'service_id': sid}
57
+ )
58
+ else:
59
+ self.token: Union[str, None] = None
60
+ self.token_parts: Union[list, None] = ByoSnap._get_token_values(
61
+ self.token) if self.token is not None else None
62
+ self.input_tag: Union[str, None] = input_tag
63
+ self.path: Union[str, None] = path
64
+ self.dockerfile: str = dockerfile
65
+ self.prefix: str = prefix
66
+ self.version: Union[str, None] = version
67
+ self.http_port: Union[int, None] = http_port
59
68
 
60
- def validate_input(self) -> ResponseType:
61
- response: ResponseType = {
62
- 'error': True,
63
- 'msg': '',
64
- 'data': []
65
- }
66
- # Check subcommand
67
- if not self.subcommand in ByoSnap.SUBCOMMANDS:
68
- response['msg'] = f"Invalid command. Valid commands are {', '.join(ByoSnap.SUBCOMMANDS)}."
69
- return response
70
- # Validate the SID
71
- if not self.sid.startswith(ByoSnap.ID_PREFIX):
72
- response['msg'] = f"Invalid Snap ID. Valid Snap IDs start with {ByoSnap.ID_PREFIX}."
73
- return response
74
- # Validation for subcommands
75
- if self.subcommand == 'create':
76
- if self.name == '':
77
- response['msg'] = f"Missing name"
78
- return response
79
- if self.language not in ByoSnap.LANGUAGES:
80
- response['msg'] = f"Invalid language. Valid languages are {', '.join(ByoSnap.LANGUAGES)}."
81
- return response
82
- if self.platform not in ByoSnap.PLATFORMS:
83
- response['msg'] = f"Invalid platform. Valid platforms are {', '.join(ByoSnap.PLATFORMS)}."
84
- return response
85
- else:
86
- # Check the token
87
- if self.token_parts is None:
88
- response['msg'] = 'Invalid token. Please reach out to your support team.'
89
- return response
90
- # Check tag
91
- if self.tag is None or len(self.tag.split()) > 1 or len(self.tag) > 25:
92
- response['msg'] = f"Tag should be a single word with maximum of 25 characters"
93
- return response
94
- if self.subcommand == 'publish-image':
95
- if not self.path:
96
- response['msg'] = f"Missing required parameter: path"
97
- return response
98
- # Check path
99
- if not os.path.isfile(f"{self.path}/{self.dockerfile}"):
100
- response['msg'] = f"Unable to find {self.dockerfile} at path {self.path}"
101
- return response
102
- elif self.subcommand == 'upload-docs':
103
- if self.path is None:
104
- response['msg'] = f"Missing required parameter: path"
105
- return response
106
- elif self.subcommand == 'publish-version':
107
- if not self.prefix:
108
- response['msg'] = f"Missing prefix"
109
- return response
110
- if not self.version:
111
- response['msg'] = f"Missing version"
112
- return response
113
- if not self.http_port:
114
- response['msg'] = f"Missing Ingress HTTP Port"
115
- return response
116
- if not self.prefix.startswith('/'):
117
- response['msg'] = f"Prefix should start with a forward slash (/)"
118
- return response
119
- if self.prefix.endswith('/'):
120
- response['msg'] = f"Prefix should not end with a forward slash (/)"
121
- return response
122
- pattern = r'^v\d+\.\d+\.\d+$'
123
- if not re.match(pattern, self.version):
124
- response['msg'] = f"Version should be in the format vX.X.X"
125
- return response
126
- if not self.http_port.isdigit():
127
- response['msg'] = f"Ingress HTTP Port should be a number"
128
- return response
129
- # Send success
130
- response['error'] = False
131
- return response
69
+ # Protected methods
70
+ @staticmethod
71
+ def _get_token_values(token: str) -> None | list:
72
+ """
73
+ Method to break open the token
74
+ """
75
+ try:
76
+ input_token = base64.b64decode(token).decode('ascii')
77
+ parts = input_token.split('|')
78
+ # url|web_app_token|service_id|ecr_repo_url|ecr_repo_username|ecr_repo_token
79
+ # url = self.token_parts[0]
80
+ # web_app_token = self.token_parts[1]
81
+ # service_id = self.token_parts[2]
82
+ # ecr_repo_url = self.token_parts[3]
83
+ # ecr_repo_username = self.token_parts[4]
84
+ # ecr_repo_token = self.token_parts[5]
85
+ # platform = self.token_parts[6]
86
+ if len(parts) >= 3:
87
+ return parts
88
+ except BinasciiError:
89
+ pass
90
+ return None
132
91
 
133
- def create(self) -> bool:
134
- with Progress(
135
- SpinnerColumn(),
136
- TextColumn("[progress.description]{task.description}"),
137
- transient=True,
138
- ) as progress:
139
- progress.add_task(description=f'Creating your snap...', total=None)
140
- try:
141
- payload = {
142
- "service_id": self.sid,
143
- "name": self.name,
144
- "description": self.desc,
145
- "platform": self.platform,
146
- "language": self.language,
147
- }
148
- res = requests.post(f"{self.base_url}/v1/snapser-api/byosnaps", json=payload, headers={'api-key': self.api_key})
149
- if res.ok:
150
- return True
151
- response_json = res.json()
152
- if "api_error_code" in response_json:
153
- if response_json['api_error_code'] == ERROR_SERVICE_VERSION_EXISTS:
154
- error('Version already exists. Please update your version and try again')
155
- if response_json['api_error_code'] == ERROR_TAG_NOT_AVAILABLE:
156
- error('Invalid tag. Please use the correct tag')
157
- else:
158
- error(f'Server error: {json.dumps(response_json, indent=2)}')
159
- except Exception as e:
160
- error("Exception: Unable to create your snap")
161
- return False
92
+ def _check_dependencies(self) -> bool:
93
+ """
94
+ Check application dependencies
95
+ """
96
+ try:
97
+ # Check dependencies
98
+ with Progress(
99
+ SpinnerColumn(),
100
+ TextColumn("[progress.description]{task.description}"),
101
+ transient=True,
102
+ ) as progress:
103
+ progress.add_task(
104
+ description='Checking dependencies...', total=None)
105
+ try:
106
+ subprocess.run([
107
+ "docker", "--version"
108
+ ], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, check=False)
109
+ except subprocess.CalledProcessError:
110
+ error('Docker not present')
111
+ return False
112
+ success('Dependencies Verified')
113
+ return True
114
+ except subprocess.CalledProcessError:
115
+ error('Unable to initialize docker')
116
+ return False
162
117
 
163
- def build(self) -> bool:
164
- # Get the data
165
- ecr_repo_url = self.token_parts[0]
166
- ecr_repo_username = self.token_parts[1]
167
- ecr_repo_token = self.token_parts[2]
168
- image_tag = f'{self.sid}.{self.tag}'
169
- full_ecr_repo_url = f'{ecr_repo_url}:{image_tag}'
170
- build_platform = ByoSnap.DEFAULT_BUILD_PLATFORM
171
- if len(self.token_parts) == 4:
172
- build_platform = self.token_parts[3]
173
- try:
174
- # Check dependencies
175
- with Progress(
176
- SpinnerColumn(),
177
- TextColumn("[progress.description]{task.description}"),
178
- transient=True,
179
- ) as progress:
180
- progress.add_task(description=f'Checking dependencies...', total=None)
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]
181
125
  try:
182
- subprocess.run([
183
- "docker", "--version"
184
- ], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
185
- except:
186
- error('Docker not present')
187
- return False
188
- success('Dependencies Verified')
126
+ # Login to Snapser Registry
127
+ with Progress(
128
+ SpinnerColumn(),
129
+ TextColumn("[progress.description]{task.description}"),
130
+ transient=True,
131
+ ) as progress:
132
+ progress.add_task(
133
+ description='Logging into Snapser Image Registry...', total=None)
134
+ if platform == 'win32':
135
+ response = subprocess.run([
136
+ 'docker', 'login', '--username', ecr_repo_username,
137
+ '--password', ecr_repo_token, ecr_repo_url
138
+ ], shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, check=False)
139
+ else:
140
+ response = subprocess.run([
141
+ f'echo "{ecr_repo_token}" | docker login '
142
+ f'--username {ecr_repo_username} --password-stdin {ecr_repo_url}'
143
+ ], shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, check=False)
144
+ if response.returncode:
145
+ error(
146
+ f'{response.returncode} - '
147
+ 'Unable to connect to the Snapser Container Repository. '
148
+ 'Please confirm if docker is running or try restarting docker'
149
+ )
150
+ return False
151
+ success('Login Successful')
152
+ return True
153
+ except subprocess.CalledProcessError:
154
+ error('Unable to initialize docker')
155
+ return False
189
156
 
190
- # Login to Snapser Registry
191
- with Progress(
192
- SpinnerColumn(),
193
- TextColumn("[progress.description]{task.description}"),
194
- transient=True,
195
- ) as progress:
196
- progress.add_task(description=f'Logging into Snapser Image Registry...', total=None)
197
- if platform == 'win32':
198
- response = subprocess.run([
199
- 'docker', 'login', '--username', ecr_repo_username, '--password', ecr_repo_token, ecr_repo_url
200
- ], shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
201
- else:
202
- response = subprocess.run([
203
- f'echo "{ecr_repo_token}" | docker login --username {ecr_repo_username} --password-stdin {ecr_repo_url}'
204
- ], shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
205
- if response.returncode:
206
- error('Unable to connect to the Snapser Container Repository. Please get the latest token from the Web app')
207
- return False
208
- success('Login Successful')
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:
164
+ # Build your snap
165
+ with Progress(
166
+ SpinnerColumn(),
167
+ TextColumn("[progress.description]{task.description}"),
168
+ transient=True,
169
+ ) as progress:
170
+ progress.add_task(
171
+ description='Building your snap...', total=None)
172
+ if platform == "win32":
173
+ response = subprocess.run([
174
+ # f"docker build --no-cache -t {tag} {path}"
175
+ 'docker', 'build', '--platform', build_platform, '-t', image_tag, self.path
176
+ ], shell=True, check=False)
177
+ # stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
178
+ else:
179
+ response = subprocess.run([
180
+ # f"docker build --no-cache -t {tag} {path}"
181
+ f"docker build --platform {build_platform} -t {image_tag} {self.path}"
182
+ ], shell=True, check=False)
183
+ # stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
184
+ if response.returncode:
185
+ error('Unable to build docker')
186
+ return False
187
+ success('Build Successful')
188
+ return True
189
+ except subprocess.CalledProcessError:
190
+ error('CLI Error')
191
+ return False
209
192
 
210
- # Build your snap
211
- with Progress(
212
- SpinnerColumn(),
213
- TextColumn("[progress.description]{task.description}"),
214
- transient=True,
215
- ) as progress:
216
- progress.add_task(description=f'Building your snap...', total=None)
217
- if platform == "win32":
218
- response = subprocess.run([
219
- #f"docker build --no-cache -t {tag} {path}"
220
- 'docker', 'build', '--platform', build_platform, '-t', image_tag, self.path
221
- ], shell=True, )#stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
222
- else:
223
- response = subprocess.run([
224
- #f"docker build --no-cache -t {tag} {path}"
225
- f"docker build --platform {build_platform} -t {image_tag} {self.path}"
226
- ], shell=True, )#stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
227
- if response.returncode:
228
- error('Unable to build docker')
229
- return False
230
- success('Build Successful')
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:
199
+ # Tag the repo
200
+ with Progress(
201
+ SpinnerColumn(),
202
+ TextColumn("[progress.description]{task.description}"),
203
+ transient=True,
204
+ ) as progress:
205
+ progress.add_task(
206
+ description='Tagging your snap...', total=None)
207
+ if platform == "win32":
208
+ response = subprocess.run([
209
+ 'docker', 'tag', image_tag, full_ecr_repo_url
210
+ ], shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, check=False)
211
+ else:
212
+ response = subprocess.run([
213
+ f"docker tag {image_tag} {full_ecr_repo_url}"
214
+ ], shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, check=False)
215
+ if response.returncode:
216
+ error('Unable to tag your snap')
217
+ return False
218
+ success('Tag Successful')
219
+ return True
220
+ except subprocess.CalledProcessError:
221
+ error('CLI Error')
222
+ return False
231
223
 
232
- # Tag the repo
233
- with Progress(
234
- SpinnerColumn(),
235
- TextColumn("[progress.description]{task.description}"),
236
- transient=True,
237
- ) as progress:
238
- progress.add_task(description=f'Tagging your snap...', total=None)
239
- if platform == "win32":
240
- response = subprocess.run([
241
- 'docker', 'tag', image_tag, full_ecr_repo_url
242
- ], shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
243
- else:
244
- response = subprocess.run([
245
- f"docker tag {image_tag} {full_ecr_repo_url}"
246
- ], shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
247
- if response.returncode:
248
- error('Unable to tag your snap')
249
- return False
250
- success('Tag Successful')
224
+ def _docker_push(self) -> bool:
225
+ """
226
+ Push the Snap image
227
+ """
228
+ ecr_repo_url = self.token_parts[0]
229
+ image_tag = f'{self.sid}.{self.input_tag}'
230
+ full_ecr_repo_url = f'{ecr_repo_url}:{image_tag}'
251
231
 
252
- return True
253
- except:
254
- error('CLI Error')
255
- return False
232
+ # Push the image
233
+ with Progress(
234
+ SpinnerColumn(),
235
+ TextColumn("[progress.description]{task.description}"),
236
+ transient=True,
237
+ ) as progress:
238
+ progress.add_task(description='Pushing your snap...', total=None)
239
+ if platform == "win32":
240
+ response = subprocess.run([
241
+ 'docker', 'push', full_ecr_repo_url
242
+ ], shell=True, check=False)
243
+ # stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
244
+ else:
245
+ response = subprocess.run([
246
+ f"docker push {full_ecr_repo_url}"
247
+ ], shell=True, check=False)
248
+ # stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
249
+ if response.returncode:
250
+ error('Unable to push your snap')
251
+ return False
252
+ success('Snap Upload Successful')
253
+ return True
256
254
 
257
- def push(self) -> bool:
258
- ecr_repo_url = self.token_parts[0]
259
- image_tag = f'{self.sid}.{self.tag}'
260
- full_ecr_repo_url = f'{ecr_repo_url}:{image_tag}'
255
+ # Public methods
261
256
 
262
- # Push the image
263
- with Progress(
264
- SpinnerColumn(),
265
- TextColumn("[progress.description]{task.description}"),
266
- transient=True,
267
- ) as progress:
268
- progress.add_task(description=f'Pushing your snap...', total=None)
269
- if platform == "win32":
270
- response = subprocess.run([
271
- 'docker', 'push', full_ecr_repo_url
272
- ], shell=True, )#stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
273
- else:
274
- response = subprocess.run([
275
- f"docker push {full_ecr_repo_url}"
276
- ], shell=True, )#stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
277
- if response.returncode:
278
- error('Unable to push your snap')
279
- return False
280
- success('Snap Upload Successful')
281
- return True
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
282
363
 
283
- def upload_docs(self) -> bool:
284
- '''
285
- Note this step is optional hence we always respond with a True
286
- '''
287
- swagger_file = f"{self.path}/swagger.json"
288
- readme_file = f"{self.path}/README.md"
289
- if os.path.isfile(swagger_file):
290
- # Push the swagger.json
291
- with Progress(
292
- SpinnerColumn(),
293
- TextColumn("[progress.description]{task.description}"),
294
- transient=True,
295
- ) as progress:
296
- progress.add_task(description=f'Uploading your API Json...', total=None)
297
- try:
298
- dfile = open(swagger_file, "rb")
299
- test_res = requests.post(f"{self.base_url}/v1/snapser-api/byosnaps/{self.sid}/docs/{self.tag}/openapispec", files = {"attachment": dfile}, headers={'api-key': self.api_key})
300
- if test_res.ok:
301
- success('Uploaded swagger.json')
302
- else:
303
- error('Unable to upload your swagger.json')
304
- except Exception as e:
305
- info('Exception: Unable to find swagger.json at ' + self.path + str(e))
306
- else:
307
- info('No swagger.json found at ' + self.path + '. Skipping swagger.json upload')
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
308
376
 
309
- # Push the README.md
310
- if os.path.isfile(readme_file):
311
- # Push the swagger.json
312
- with Progress(
313
- SpinnerColumn(),
314
- TextColumn("[progress.description]{task.description}"),
315
- transient=True,
316
- ) as progress:
317
- progress.add_task(description=f'Uploading your README...', total=None)
318
- try:
319
- dfile = open(readme_file, "rb")
320
- test_res = requests.post(f"{self.base_url}/v1/snapser-api/byosnaps/{self.sid}/docs/{self.tag}/markdown", files = {"attachment": dfile}, headers={'api-key': self.api_key})
321
- if test_res.ok:
322
- success('Uploaded README.md')
323
- else:
324
- error('Unable to upload your README.md')
325
- except Exception as e:
326
- info('Exception: Unable to find README.md at ' + self.path + str(e))
327
- else:
328
- info('No README.md found at ' + self.path + '. Skipping README.md upload')
329
- return True
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
330
389
 
331
- def publish_image(self) -> bool:
332
- if not self.build() or not self.push() or not self.upload_docs():
333
- return False
334
- return True
390
+ def upload_docs(self) -> bool:
391
+ '''
392
+ Note this step is optional hence we always respond with a True
393
+ '''
394
+ swagger_file = f"{self.path}/swagger.json"
395
+ readme_file = f"{self.path}/README.md"
396
+ if os.path.isfile(swagger_file):
397
+ # Push the swagger.json
398
+ with Progress(
399
+ SpinnerColumn(),
400
+ TextColumn("[progress.description]{task.description}"),
401
+ transient=True,
402
+ ) as progress:
403
+ progress.add_task(
404
+ description='Uploading your API Json...', total=None)
405
+ try:
406
+ attachment_file = open(swagger_file, "rb")
407
+ url = (
408
+ f"{self.base_url}/v1/snapser-api/byosnaps/"
409
+ f"{self.sid}/docs/{self.input_tag}/openapispec"
410
+ )
411
+ test_res = requests.post(
412
+ url, files={"attachment": attachment_file},
413
+ headers={'api-key': self.api_key},
414
+ timeout=SERVER_CALL_TIMEOUT
415
+ )
416
+ if test_res.ok:
417
+ success('Uploaded swagger.json')
418
+ else:
419
+ error('Unable to upload your swagger.json')
420
+ except RequestException as e:
421
+ info(
422
+ f'Exception: Unable to find swagger.json at {self.path} {e}'
423
+ )
424
+ else:
425
+ info('No swagger.json found at ' + self.path +
426
+ '. Skipping swagger.json upload')
335
427
 
336
- def publish_version(self) -> bool:
337
- with Progress(
338
- SpinnerColumn(),
339
- TextColumn("[progress.description]{task.description}"),
340
- transient=True,
341
- ) as progress:
342
- progress.add_task(description=f'Publishing your snap...', total=None)
343
- try:
344
- payload = {
345
- "version": self.version,
346
- "image_tag": self.tag,
347
- "base_url": f"{self.prefix}/{self.sid}",
348
- "http_port": self.http_port,
349
- }
350
- res = requests.post(f"{self.base_url}/v1/snapser-api/byosnaps/{self.sid}/versions", json=payload, headers={'api-key': self.api_key})
351
- if res.ok:
352
- return True
353
- response_json = res.json()
354
- if "api_error_code" in response_json:
355
- if response_json['api_error_code'] == ERROR_SERVICE_VERSION_EXISTS:
356
- error('Version already exists. Please update your version and try again')
357
- if response_json['api_error_code'] == ERROR_TAG_NOT_AVAILABLE:
358
- error('Invalid tag. Please use the correct tag')
428
+ # Push the README.md
429
+ if os.path.isfile(readme_file):
430
+ # Push the swagger.json
431
+ with Progress(
432
+ SpinnerColumn(),
433
+ TextColumn("[progress.description]{task.description}"),
434
+ transient=True,
435
+ ) as progress:
436
+ progress.add_task(
437
+ description='Uploading your README...', total=None)
438
+ try:
439
+ attachment_file = open(readme_file, "rb")
440
+ url = (
441
+ f"{self.base_url}/v1/snapser-api/byosnaps/"
442
+ f"{self.sid}/docs/{self.input_tag}/markdown"
443
+ )
444
+ test_res = requests.post(
445
+ url, files={"attachment": attachment_file},
446
+ headers={'api-key': self.api_key},
447
+ timeout=SERVER_CALL_TIMEOUT
448
+ )
449
+ if test_res.ok:
450
+ success('Uploaded README.md')
451
+ else:
452
+ error('Unable to upload your README.md')
453
+ except RequestException as e:
454
+ info(
455
+ f'Exception: Unable to find README.md at {self.path} {str(e)}'
456
+ )
359
457
  else:
360
- error(f'Server error: {json.dumps(response_json, indent=2)}')
361
- except Exception as e:
362
- error("Exception: Unable to publish a version for your snap")
363
- return False
458
+ info(
459
+ f'No README.md found at {self.path}. Skipping README.md upload'
460
+ )
461
+ return True
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
+
512
+ def publish_image(self) -> bool:
513
+ """
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
521
+ """
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():
525
+ return False
526
+ return True
364
527
 
528
+ def publish_version(self) -> bool:
529
+ """
530
+ Publish the version
531
+ """
532
+ with Progress(
533
+ SpinnerColumn(),
534
+ TextColumn("[progress.description]{task.description}"),
535
+ transient=True,
536
+ ) as progress:
537
+ progress.add_task(
538
+ description='Publishing your snap...', total=None)
539
+ try:
540
+ payload = {
541
+ "version": self.version,
542
+ "image_tag": self.input_tag,
543
+ "base_url": f"{self.prefix}/{self.sid}",
544
+ "http_port": self.http_port,
545
+ }
546
+ res = requests.post(
547
+ f"{self.base_url}/v1/snapser-api/byosnaps/{self.sid}/versions",
548
+ json=payload, headers={'api-key': self.api_key},
549
+ timeout=SERVER_CALL_TIMEOUT
550
+ )
551
+ if res.ok:
552
+ return True
553
+ response_json = res.json()
554
+ if "api_error_code" in response_json:
555
+ if response_json['api_error_code'] == ERROR_SERVICE_VERSION_EXISTS:
556
+ error(
557
+ 'Version already exists. Please update your version and try again'
558
+ )
559
+ if response_json['api_error_code'] == ERROR_TAG_NOT_AVAILABLE:
560
+ error('Invalid tag. Please use the correct tag')
561
+ else:
562
+ error(
563
+ f'Server error: {json.dumps(response_json, indent=2)}'
564
+ )
565
+ except RequestException as e:
566
+ error(
567
+ f'Exception: Unable to publish a version for your snap {e}'
568
+ )
569
+ return False