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.
- snapctl/__main__.py +3 -0
- snapctl/commands/byogs.py +443 -311
- snapctl/commands/byosnap.py +540 -335
- snapctl/commands/snapend.py +265 -182
- snapctl/config/constants.py +6 -1
- snapctl/config/endpoints.py +8 -5
- snapctl/config/hashes.py +13 -3
- snapctl/main.py +396 -183
- snapctl/types/definitions.py +11 -3
- snapctl/utils/echo.py +18 -3
- snapctl/utils/helper.py +33 -20
- {snapctl-0.4.5.dist-info → snapctl-0.22.1.dist-info}/METADATA +218 -67
- snapctl-0.22.1.dist-info/RECORD +20 -0
- {snapctl-0.4.5.dist-info → snapctl-0.22.1.dist-info}/WHEEL +1 -1
- snapctl-0.4.5.dist-info/RECORD +0 -20
- {snapctl-0.4.5.dist-info → snapctl-0.22.1.dist-info}/entry_points.txt +0 -0
snapctl/commands/byosnap.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
error('
|
|
157
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|