snapctl 0.32.0__py3-none-any.whl → 0.32.2__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/commands/byogs.py +175 -181
- snapctl/commands/byosnap.py +378 -371
- snapctl/commands/game.py +63 -57
- snapctl/commands/generate.py +48 -32
- snapctl/commands/snapend.py +361 -281
- snapctl/config/constants.py +72 -8
- snapctl/main.py +100 -135
- snapctl/types/definitions.py +20 -4
- snapctl/utils/echo.py +10 -2
- snapctl/utils/helper.py +60 -3
- {snapctl-0.32.0.dist-info → snapctl-0.32.2.dist-info}/METADATA +120 -8
- snapctl-0.32.2.dist-info/RECORD +23 -0
- snapctl-0.32.0.dist-info/RECORD +0 -23
- {snapctl-0.32.0.dist-info → snapctl-0.32.2.dist-info}/LICENSE +0 -0
- {snapctl-0.32.0.dist-info → snapctl-0.32.2.dist-info}/WHEEL +0 -0
- {snapctl-0.32.0.dist-info → snapctl-0.32.2.dist-info}/entry_points.txt +0 -0
snapctl/commands/byosnap.py
CHANGED
|
@@ -14,11 +14,16 @@ from requests.exceptions import RequestException
|
|
|
14
14
|
|
|
15
15
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
16
16
|
from snapctl.config.constants import SERVER_CALL_TIMEOUT
|
|
17
|
-
from snapctl.config.constants import
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
from snapctl.config.constants import HTTP_ERROR_SERVICE_VERSION_EXISTS, \
|
|
18
|
+
HTTP_ERROR_TAG_NOT_AVAILABLE, HTTP_ERROR_ADD_ON_NOT_ENABLED, SNAPCTL_INPUT_ERROR, \
|
|
19
|
+
SNAPCTL_BYOSNAP_DEPENDENCY_MISSING, SNAPCTL_BYOSNAP_ECR_LOGIN_ERROR, \
|
|
20
|
+
SNAPCTL_BYOSNAP_BUILD_ERROR, SNAPCTL_BYOSNAP_TAG_ERROR, SNAPCTL_BYOSNAP_PUBLISH_IMAGE_ERROR, \
|
|
21
|
+
SNAPCTL_BYOSNAP_PUBLISH_IMAGE_DUPLICATE_TAG_ERROR, \
|
|
22
|
+
SNAPCTL_BYOSNAP_CREATE_DUPLICATE_NAME_ERROR, SNAPCTL_BYOSNAP_CREATE_PERMISSION_ERROR, \
|
|
23
|
+
SNAPCTL_BYOSNAP_CREATE_ERROR, SNAPCTL_BYOSNAP_PUBLISH_VERSION_DUPLICATE_TAG_ERROR, \
|
|
24
|
+
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
|
|
22
27
|
|
|
23
28
|
|
|
24
29
|
class ByoSnap:
|
|
@@ -27,8 +32,7 @@ class ByoSnap:
|
|
|
27
32
|
"""
|
|
28
33
|
ID_PREFIX = 'byosnap-'
|
|
29
34
|
SUBCOMMANDS = [
|
|
30
|
-
'
|
|
31
|
-
'create', 'publish-image', 'publish-version',
|
|
35
|
+
'create', 'publish-image', 'publish-version', 'upload-docs',
|
|
32
36
|
]
|
|
33
37
|
PLATFORMS = ['linux/arm64', 'linux/amd64']
|
|
34
38
|
LANGUAGES = ['go', 'python', 'ruby', 'c#', 'c++', 'rust', 'java', 'node']
|
|
@@ -68,6 +72,8 @@ class ByoSnap:
|
|
|
68
72
|
self.version: Union[str, None] = version
|
|
69
73
|
self.http_port: Union[int, None] = http_port
|
|
70
74
|
self.byosnap_profile: Union[str, None] = byosnap_profile
|
|
75
|
+
# Validate the input
|
|
76
|
+
self.validate_input()
|
|
71
77
|
|
|
72
78
|
# Protected methods
|
|
73
79
|
@staticmethod
|
|
@@ -92,155 +98,156 @@ class ByoSnap:
|
|
|
92
98
|
pass
|
|
93
99
|
return None
|
|
94
100
|
|
|
95
|
-
def _check_dependencies(self) ->
|
|
101
|
+
def _check_dependencies(self) -> None:
|
|
96
102
|
"""
|
|
97
103
|
Check application dependencies
|
|
98
104
|
"""
|
|
105
|
+
progress = Progress(
|
|
106
|
+
SpinnerColumn(),
|
|
107
|
+
TextColumn("[progress.description]{task.description}"),
|
|
108
|
+
transient=True,
|
|
109
|
+
)
|
|
110
|
+
progress.start()
|
|
111
|
+
progress.add_task(
|
|
112
|
+
description='Checking dependencies...', total=None)
|
|
99
113
|
try:
|
|
100
114
|
# Check dependencies
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
description='Checking dependencies...', total=None)
|
|
108
|
-
try:
|
|
109
|
-
subprocess.run([
|
|
110
|
-
"docker", "--version"
|
|
111
|
-
], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, check=False)
|
|
112
|
-
except subprocess.CalledProcessError:
|
|
113
|
-
error('Docker not present')
|
|
114
|
-
return False
|
|
115
|
-
success('Dependencies Verified')
|
|
116
|
-
return True
|
|
115
|
+
result = subprocess.run([
|
|
116
|
+
"docker", "info"
|
|
117
|
+
], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, check=False)
|
|
118
|
+
if not result.returncode:
|
|
119
|
+
return snapctl_success('BYOSnap dependencies verified',
|
|
120
|
+
progress, no_exit=True)
|
|
117
121
|
except subprocess.CalledProcessError:
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
snapctl_error('Snapctl Exception',
|
|
123
|
+
SNAPCTL_BYOSNAP_DEPENDENCY_MISSING, progress)
|
|
124
|
+
snapctl_error('Docker not running. Please start docker.',
|
|
125
|
+
SNAPCTL_BYOSNAP_DEPENDENCY_MISSING, progress)
|
|
120
126
|
|
|
121
|
-
def _docker_login(self) ->
|
|
127
|
+
def _docker_login(self) -> None:
|
|
122
128
|
"""
|
|
123
129
|
Docker Login
|
|
124
130
|
"""
|
|
125
131
|
ecr_repo_url = self.token_parts[0]
|
|
126
132
|
ecr_repo_username = self.token_parts[1]
|
|
127
133
|
ecr_repo_token = self.token_parts[2]
|
|
134
|
+
progress = Progress(
|
|
135
|
+
SpinnerColumn(),
|
|
136
|
+
TextColumn("[progress.description]{task.description}"),
|
|
137
|
+
transient=True,
|
|
138
|
+
)
|
|
139
|
+
progress.start()
|
|
140
|
+
progress.add_task(
|
|
141
|
+
description='Logging into Snapser Image Registry...', total=None)
|
|
128
142
|
try:
|
|
129
143
|
# Login to Snapser Registry
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
response = subprocess.run([
|
|
144
|
-
f'echo "{ecr_repo_token}" | docker login '
|
|
145
|
-
f'--username {ecr_repo_username} --password-stdin {ecr_repo_url}'
|
|
146
|
-
], shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, check=False)
|
|
147
|
-
if response.returncode:
|
|
148
|
-
error(
|
|
149
|
-
f'{response.returncode} - '
|
|
150
|
-
'Unable to connect to the Snapser Container Repository. '
|
|
151
|
-
'Please confirm if docker is running or try restarting docker'
|
|
152
|
-
)
|
|
153
|
-
return False
|
|
154
|
-
success('Login Successful')
|
|
155
|
-
return True
|
|
144
|
+
if platform == 'win32':
|
|
145
|
+
response = subprocess.run([
|
|
146
|
+
'docker', 'login', '--username', ecr_repo_username,
|
|
147
|
+
'--password', ecr_repo_token, ecr_repo_url
|
|
148
|
+
], shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, check=False)
|
|
149
|
+
else:
|
|
150
|
+
response = subprocess.run([
|
|
151
|
+
f'echo "{ecr_repo_token}" | docker login '
|
|
152
|
+
f'--username {ecr_repo_username} --password-stdin {ecr_repo_url}'
|
|
153
|
+
], shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, check=False)
|
|
154
|
+
if not response.returncode:
|
|
155
|
+
return snapctl_success('BYOSnap ECR login successful',
|
|
156
|
+
progress, no_exit=True)
|
|
156
157
|
except subprocess.CalledProcessError:
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
snapctl_error('Snapctl Exception',
|
|
159
|
+
SNAPCTL_BYOSNAP_ECR_LOGIN_ERROR, progress)
|
|
160
|
+
snapctl_error('BYOSnap ECR login failure',
|
|
161
|
+
SNAPCTL_BYOSNAP_ECR_LOGIN_ERROR, progress)
|
|
159
162
|
|
|
160
|
-
def _docker_build(self) ->
|
|
163
|
+
def _docker_build(self) -> None:
|
|
161
164
|
# Get the data
|
|
162
165
|
image_tag = f'{self.sid}.{self.input_tag}'
|
|
163
166
|
build_platform = ByoSnap.DEFAULT_BUILD_PLATFORM
|
|
164
167
|
if len(self.token_parts) == 4:
|
|
165
168
|
build_platform = self.token_parts[3]
|
|
169
|
+
progress = Progress(
|
|
170
|
+
SpinnerColumn(),
|
|
171
|
+
TextColumn("[progress.description]{task.description}"),
|
|
172
|
+
transient=True,
|
|
173
|
+
)
|
|
174
|
+
progress.start()
|
|
175
|
+
progress.add_task(
|
|
176
|
+
description='Building your snap...', total=None)
|
|
166
177
|
try:
|
|
167
178
|
# Build your snap
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
f"docker build --platform {build_platform} -t {image_tag} -f {docker_file_path} {self.path}"
|
|
187
|
-
], shell=True, check=False)
|
|
188
|
-
# stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
|
|
189
|
-
if response.returncode:
|
|
190
|
-
error('Unable to build docker')
|
|
191
|
-
return False
|
|
192
|
-
success('Build Successful')
|
|
193
|
-
return True
|
|
179
|
+
docker_file_path = os.path.join(self.path, self.dockerfile)
|
|
180
|
+
if platform == "win32":
|
|
181
|
+
response = subprocess.run([
|
|
182
|
+
# f"docker build --no-cache -t {tag} {path}"
|
|
183
|
+
'docker', 'build', '--platform', build_platform, '-t', image_tag,
|
|
184
|
+
'-f', docker_file_path, self.path
|
|
185
|
+
], shell=True, check=False)
|
|
186
|
+
# stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
|
|
187
|
+
else:
|
|
188
|
+
response = subprocess.run([
|
|
189
|
+
# f"docker build --no-cache -t {tag} {path}"
|
|
190
|
+
f"docker build --platform {build_platform} -t {image_tag} "
|
|
191
|
+
f"-f {docker_file_path} {self.path}"
|
|
192
|
+
], shell=True, check=False)
|
|
193
|
+
# stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
|
|
194
|
+
if not response.returncode:
|
|
195
|
+
return snapctl_success('BYOSnap build successful',
|
|
196
|
+
progress, no_exit=True)
|
|
194
197
|
except subprocess.CalledProcessError:
|
|
195
|
-
|
|
196
|
-
|
|
198
|
+
snapctl_error('Snapctl Exception',
|
|
199
|
+
SNAPCTL_BYOSNAP_BUILD_ERROR, progress)
|
|
200
|
+
snapctl_error('BYOSnap build failure',
|
|
201
|
+
SNAPCTL_BYOSNAP_BUILD_ERROR, progress)
|
|
197
202
|
|
|
198
|
-
def _docker_tag(self) ->
|
|
203
|
+
def _docker_tag(self) -> None:
|
|
199
204
|
# Get the data
|
|
200
205
|
ecr_repo_url = self.token_parts[0]
|
|
201
206
|
image_tag = f'{self.sid}.{self.input_tag}'
|
|
202
207
|
full_ecr_repo_url = f'{ecr_repo_url}:{image_tag}'
|
|
208
|
+
progress = Progress(
|
|
209
|
+
SpinnerColumn(),
|
|
210
|
+
TextColumn("[progress.description]{task.description}"),
|
|
211
|
+
transient=True,
|
|
212
|
+
)
|
|
213
|
+
progress.start()
|
|
214
|
+
progress.add_task(
|
|
215
|
+
description='Tagging your snap...', total=None)
|
|
203
216
|
try:
|
|
204
217
|
# Tag the repo
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
else:
|
|
217
|
-
response = subprocess.run([
|
|
218
|
-
f"docker tag {image_tag} {full_ecr_repo_url}"
|
|
219
|
-
], shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, check=False)
|
|
220
|
-
if response.returncode:
|
|
221
|
-
error('Unable to tag your snap')
|
|
222
|
-
return False
|
|
223
|
-
success('Tag Successful')
|
|
224
|
-
return True
|
|
218
|
+
if platform == "win32":
|
|
219
|
+
response = subprocess.run([
|
|
220
|
+
'docker', 'tag', image_tag, full_ecr_repo_url
|
|
221
|
+
], shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, check=False)
|
|
222
|
+
else:
|
|
223
|
+
response = subprocess.run([
|
|
224
|
+
f"docker tag {image_tag} {full_ecr_repo_url}"
|
|
225
|
+
], shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, check=False)
|
|
226
|
+
if not response.returncode:
|
|
227
|
+
return snapctl_success('BYOSnap tag successful',
|
|
228
|
+
progress, no_exit=True)
|
|
225
229
|
except subprocess.CalledProcessError:
|
|
226
|
-
|
|
227
|
-
|
|
230
|
+
snapctl_error('Snapctl Exception',
|
|
231
|
+
SNAPCTL_BYOSNAP_TAG_ERROR, progress)
|
|
232
|
+
snapctl_error('BYOSnap tag failure',
|
|
233
|
+
SNAPCTL_BYOSNAP_TAG_ERROR, progress)
|
|
228
234
|
|
|
229
235
|
def _docker_push(self) -> bool:
|
|
230
236
|
"""
|
|
231
237
|
Push the Snap image
|
|
232
238
|
"""
|
|
233
|
-
|
|
234
|
-
image_tag = f'{self.sid}.{self.input_tag}'
|
|
235
|
-
full_ecr_repo_url = f'{ecr_repo_url}:{image_tag}'
|
|
236
|
-
|
|
237
|
-
# Push the image
|
|
238
|
-
with Progress(
|
|
239
|
+
progress = Progress(
|
|
239
240
|
SpinnerColumn(),
|
|
240
241
|
TextColumn("[progress.description]{task.description}"),
|
|
241
242
|
transient=True,
|
|
242
|
-
)
|
|
243
|
-
|
|
243
|
+
)
|
|
244
|
+
progress.start()
|
|
245
|
+
progress.add_task(description='Pushing your snap...', total=None)
|
|
246
|
+
try:
|
|
247
|
+
# Push the image
|
|
248
|
+
ecr_repo_url = self.token_parts[0]
|
|
249
|
+
image_tag = f'{self.sid}.{self.input_tag}'
|
|
250
|
+
full_ecr_repo_url = f'{ecr_repo_url}:{image_tag}'
|
|
244
251
|
if platform == "win32":
|
|
245
252
|
response = subprocess.run([
|
|
246
253
|
'docker', 'push', full_ecr_repo_url
|
|
@@ -251,196 +258,192 @@ class ByoSnap:
|
|
|
251
258
|
f"docker push {full_ecr_repo_url}"
|
|
252
259
|
], shell=True, check=False)
|
|
253
260
|
# stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
|
|
254
|
-
if response.returncode:
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
261
|
+
if not response.returncode:
|
|
262
|
+
return snapctl_success('BYOSnap upload successful',
|
|
263
|
+
progress, no_exit=True)
|
|
264
|
+
except subprocess.CalledProcessError:
|
|
265
|
+
snapctl_error('Snapctl Exception',
|
|
266
|
+
SNAPCTL_BYOSNAP_PUBLISH_IMAGE_ERROR, progress)
|
|
267
|
+
snapctl_error('BYOSnap upload failure. Duplicate image error.',
|
|
268
|
+
SNAPCTL_BYOSNAP_PUBLISH_IMAGE_DUPLICATE_TAG_ERROR, progress)
|
|
259
269
|
|
|
260
|
-
def _validate_byosnap_profile(self) ->
|
|
270
|
+
def _validate_byosnap_profile(self) -> None:
|
|
261
271
|
"""
|
|
262
272
|
Validate the BYOSnap profile
|
|
263
273
|
"""
|
|
264
|
-
response = {
|
|
265
|
-
'error': True,
|
|
266
|
-
'message': ''
|
|
267
|
-
}
|
|
268
274
|
if not self.byosnap_profile:
|
|
269
|
-
|
|
270
|
-
return response
|
|
275
|
+
snapctl_error("Missing BYOSnap profile path", SNAPCTL_INPUT_ERROR)
|
|
271
276
|
if not os.path.isfile(self.byosnap_profile):
|
|
272
|
-
|
|
277
|
+
snapctl_error(
|
|
273
278
|
"Unable to find BYOSnap profile "
|
|
274
|
-
F"JSON at path {self.byosnap_profile}"
|
|
279
|
+
F"JSON at path {self.byosnap_profile}",
|
|
280
|
+
SNAPCTL_INPUT_ERROR
|
|
275
281
|
)
|
|
276
|
-
|
|
282
|
+
profile_data = None
|
|
277
283
|
with open(self.byosnap_profile, 'rb') as file:
|
|
278
284
|
try:
|
|
279
285
|
profile_data = json.load(file)
|
|
280
286
|
except json.JSONDecodeError:
|
|
281
|
-
|
|
282
|
-
|
|
287
|
+
pass
|
|
288
|
+
if not profile_data:
|
|
289
|
+
snapctl_error(
|
|
290
|
+
'Invalid BYOSnap profile JSON. Please check the JSON structure',
|
|
291
|
+
SNAPCTL_INPUT_ERROR
|
|
292
|
+
)
|
|
293
|
+
if 'dev_template' not in profile_data or \
|
|
294
|
+
'stage_template' not in profile_data or \
|
|
295
|
+
'prod_template' not in profile_data:
|
|
296
|
+
snapctl_error(
|
|
297
|
+
'Invalid BYOSnap profile JSON. Please check the JSON structure',
|
|
298
|
+
SNAPCTL_INPUT_ERROR
|
|
299
|
+
)
|
|
300
|
+
for profile in ['dev_template', 'stage_template', 'prod_template']:
|
|
301
|
+
if 'cpu' not in profile_data[profile] or \
|
|
302
|
+
'memory' not in profile_data[profile] or \
|
|
303
|
+
'cmd' not in profile_data[profile] or \
|
|
304
|
+
'args' not in profile_data[profile] or \
|
|
305
|
+
'env_params' not in profile_data[profile]:
|
|
306
|
+
snapctl_error(
|
|
307
|
+
'Invalid BYOSnap profile JSON. Please check the JSON structure',
|
|
308
|
+
SNAPCTL_INPUT_ERROR
|
|
283
309
|
)
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
'
|
|
288
|
-
|
|
289
|
-
|
|
310
|
+
if profile_data[profile]['cpu'] not in ByoSnap.VALID_CPU_MARKS:
|
|
311
|
+
snapctl_error(
|
|
312
|
+
'Invalid CPU value in BYOSnap profile. '
|
|
313
|
+
f'Valid values are {", ".join(map(str, ByoSnap.VALID_CPU_MARKS))}',
|
|
314
|
+
SNAPCTL_INPUT_ERROR
|
|
315
|
+
)
|
|
316
|
+
if profile_data[profile]['memory'] not in ByoSnap.VALID_MEMORY_MARKS:
|
|
317
|
+
snapctl_error(
|
|
318
|
+
'Invalid Memory value in BYOSnap profile. '
|
|
319
|
+
f'Valid values are {", ".join(map(str, ByoSnap.VALID_MEMORY_MARKS))}',
|
|
320
|
+
SNAPCTL_INPUT_ERROR
|
|
290
321
|
)
|
|
291
|
-
return response
|
|
292
|
-
for profile in ['dev_template', 'stage_template', 'prod_template']:
|
|
293
|
-
if 'cpu' not in profile_data[profile] or \
|
|
294
|
-
'memory' not in profile_data[profile] or \
|
|
295
|
-
'cmd' not in profile_data[profile] or \
|
|
296
|
-
'args' not in profile_data[profile] or \
|
|
297
|
-
'env_params' not in profile_data[profile]:
|
|
298
|
-
response['message'] = (
|
|
299
|
-
'Invalid BYOSnap profile JSON. Please check the JSON structure'
|
|
300
|
-
)
|
|
301
|
-
return response
|
|
302
|
-
if profile_data[profile]['cpu'] not in ByoSnap.VALID_CPU_MARKS:
|
|
303
|
-
response['message'] = (
|
|
304
|
-
'Invalid CPU value in BYOSnap profile. '
|
|
305
|
-
f'Valid values are {", ".join(ByoSnap.VALID_CPU_MARKS)}'
|
|
306
|
-
)
|
|
307
|
-
return response
|
|
308
|
-
if profile_data[profile]['memory'] not in ByoSnap.VALID_MEMORY_MARKS:
|
|
309
|
-
response['message'] = (
|
|
310
|
-
'Invalid Memory value in BYOSnap profile. '
|
|
311
|
-
f'Valid values are {", ".join(ByoSnap.VALID_MEMORY_MARKS)}'
|
|
312
|
-
)
|
|
313
|
-
return response
|
|
314
|
-
response['error'] = False
|
|
315
|
-
return response
|
|
316
322
|
|
|
317
323
|
# Public methods
|
|
318
324
|
|
|
319
325
|
# Validator
|
|
320
|
-
def validate_input(self) ->
|
|
326
|
+
def validate_input(self) -> None:
|
|
321
327
|
"""
|
|
322
328
|
Validator
|
|
323
329
|
"""
|
|
324
|
-
response: ResponseType = {
|
|
325
|
-
'error': True,
|
|
326
|
-
'msg': '',
|
|
327
|
-
'data': []
|
|
328
|
-
}
|
|
329
330
|
# Check API Key and Base URL
|
|
330
331
|
if not self.api_key or self.base_url == '':
|
|
331
|
-
|
|
332
|
-
return response
|
|
332
|
+
snapctl_error("Missing API Key.", SNAPCTL_INPUT_ERROR)
|
|
333
333
|
# Check subcommand
|
|
334
334
|
if not self.subcommand in ByoSnap.SUBCOMMANDS:
|
|
335
|
-
|
|
336
|
-
"Invalid command. Valid commands "
|
|
337
|
-
f"are {', '.join(ByoSnap.SUBCOMMANDS)}."
|
|
335
|
+
snapctl_error(
|
|
336
|
+
"Invalid command. Valid commands "
|
|
337
|
+
f"are {', '.join(ByoSnap.SUBCOMMANDS)}.",
|
|
338
|
+
SNAPCTL_INPUT_ERROR
|
|
338
339
|
)
|
|
339
|
-
return response
|
|
340
340
|
# Validate the SID
|
|
341
341
|
if not self.sid.startswith(ByoSnap.ID_PREFIX):
|
|
342
|
-
|
|
343
|
-
|
|
342
|
+
snapctl_error(
|
|
343
|
+
f"Invalid Snap ID. Valid Snap IDs start with {ByoSnap.ID_PREFIX}.",
|
|
344
|
+
SNAPCTL_INPUT_ERROR
|
|
345
|
+
)
|
|
344
346
|
if len(self.sid) > ByoSnap.SID_CHARACTER_LIMIT:
|
|
345
|
-
|
|
347
|
+
snapctl_error(
|
|
346
348
|
"Invalid Snap ID. "
|
|
347
|
-
f"Snap ID should be less than {ByoSnap.SID_CHARACTER_LIMIT} characters"
|
|
349
|
+
f"Snap ID should be less than {ByoSnap.SID_CHARACTER_LIMIT} characters",
|
|
350
|
+
SNAPCTL_INPUT_ERROR
|
|
348
351
|
)
|
|
349
|
-
return response
|
|
350
352
|
# Validation for subcommands
|
|
351
353
|
if self.subcommand == 'create':
|
|
352
354
|
if self.name == '':
|
|
353
|
-
|
|
354
|
-
|
|
355
|
+
snapctl_error("Missing name", SNAPCTL_INPUT_ERROR)
|
|
356
|
+
if not self.language:
|
|
357
|
+
snapctl_error("Missing language", SNAPCTL_INPUT_ERROR)
|
|
355
358
|
if self.language not in ByoSnap.LANGUAGES:
|
|
356
|
-
|
|
359
|
+
snapctl_error(
|
|
357
360
|
"Invalid language. Valid languages are "
|
|
358
|
-
f"{', '.join(ByoSnap.LANGUAGES)}."
|
|
361
|
+
f"{', '.join(ByoSnap.LANGUAGES)}.",
|
|
362
|
+
SNAPCTL_INPUT_ERROR
|
|
359
363
|
)
|
|
360
|
-
return response
|
|
361
364
|
if self.platform_type not in ByoSnap.PLATFORMS:
|
|
362
|
-
|
|
365
|
+
snapctl_error(
|
|
363
366
|
"Invalid platform. Valid platforms are "
|
|
364
|
-
f"{', '.join(ByoSnap.PLATFORMS)}."
|
|
367
|
+
f"{', '.join(ByoSnap.PLATFORMS)}.",
|
|
368
|
+
SNAPCTL_INPUT_ERROR
|
|
365
369
|
)
|
|
366
|
-
return response
|
|
367
370
|
else:
|
|
368
371
|
# Check the token
|
|
369
372
|
if self.token_parts is None:
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
if self.input_tag is None or len(self.input_tag.split()) > 1 or \
|
|
374
|
-
len(self.input_tag) > ByoSnap.TAG_CHARACTER_LIMIT:
|
|
375
|
-
response['msg'] = (
|
|
376
|
-
"Tag should be a single word with maximum of "
|
|
377
|
-
f"{ByoSnap.TAG_CHARACTER_LIMIT} characters"
|
|
378
|
-
)
|
|
379
|
-
return response
|
|
380
|
-
if self.subcommand == 'build' or self.subcommand == 'publish-image':
|
|
373
|
+
snapctl_error('Invalid token. Please reach out to your support team.',
|
|
374
|
+
SNAPCTL_INPUT_ERROR)
|
|
375
|
+
if self.subcommand in ['build', 'publish-image']:
|
|
381
376
|
if not self.input_tag:
|
|
382
|
-
|
|
383
|
-
|
|
377
|
+
snapctl_error(
|
|
378
|
+
"Missing required parameter: tag", SNAPCTL_INPUT_ERROR)
|
|
379
|
+
if len(self.input_tag.split()) > 1 or \
|
|
380
|
+
len(self.input_tag) > ByoSnap.TAG_CHARACTER_LIMIT:
|
|
381
|
+
snapctl_error(
|
|
382
|
+
"Tag should be a single word with maximum of "
|
|
383
|
+
f"{ByoSnap.TAG_CHARACTER_LIMIT} characters",
|
|
384
|
+
SNAPCTL_INPUT_ERROR
|
|
385
|
+
)
|
|
384
386
|
if not self.path:
|
|
385
|
-
|
|
386
|
-
|
|
387
|
+
snapctl_error("Missing required parameter: path",
|
|
388
|
+
SNAPCTL_INPUT_ERROR)
|
|
387
389
|
# Check path
|
|
388
390
|
if not os.path.isfile(f"{self.path}/{self.dockerfile}"):
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
391
|
+
snapctl_error(
|
|
392
|
+
f"Unable to find {self.dockerfile} at path {self.path}",
|
|
393
|
+
SNAPCTL_INPUT_ERROR)
|
|
394
|
+
# elif self.subcommand == 'push':
|
|
395
|
+
# if not self.input_tag:
|
|
396
|
+
# error("Missing required parameter: tag", SNAPCTL_INPUT_ERROR)
|
|
397
|
+
# raise typer.Exit(code=SNAPCTL_INPUT_ERROR)
|
|
395
398
|
elif self.subcommand == 'upload-docs':
|
|
396
399
|
if self.path is None:
|
|
397
|
-
|
|
398
|
-
|
|
400
|
+
snapctl_error(
|
|
401
|
+
"Missing required parameter: path", SNAPCTL_INPUT_ERROR)
|
|
399
402
|
elif self.subcommand == 'publish-version':
|
|
400
|
-
if not self.
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
if
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
403
|
+
if not self.input_tag:
|
|
404
|
+
snapctl_error(
|
|
405
|
+
"Missing required parameter: tag", SNAPCTL_INPUT_ERROR)
|
|
406
|
+
if len(self.input_tag.split()) > 1 or len(self.input_tag) > ByoSnap.TAG_CHARACTER_LIMIT:
|
|
407
|
+
snapctl_error(
|
|
408
|
+
"Tag should be a single word with maximum of "
|
|
409
|
+
f"{ByoSnap.TAG_CHARACTER_LIMIT} characters",
|
|
410
|
+
SNAPCTL_INPUT_ERROR
|
|
411
|
+
)
|
|
412
|
+
if not self.prefix or self.prefix == '':
|
|
413
|
+
snapctl_error("Missing prefix", SNAPCTL_INPUT_ERROR)
|
|
409
414
|
if not self.prefix.startswith('/'):
|
|
410
|
-
|
|
411
|
-
|
|
415
|
+
snapctl_error("Prefix should start with a forward slash (/)",
|
|
416
|
+
SNAPCTL_INPUT_ERROR)
|
|
412
417
|
if self.prefix.endswith('/'):
|
|
413
|
-
|
|
414
|
-
|
|
418
|
+
snapctl_error("Prefix should not end with a forward slash (/)",
|
|
419
|
+
SNAPCTL_INPUT_ERROR)
|
|
420
|
+
if not self.version:
|
|
421
|
+
snapctl_error("Missing version", SNAPCTL_INPUT_ERROR)
|
|
415
422
|
pattern = r'^v\d+\.\d+\.\d+$'
|
|
416
423
|
if not re.match(pattern, self.version):
|
|
417
|
-
|
|
418
|
-
|
|
424
|
+
snapctl_error("Version should be in the format vX.X.X",
|
|
425
|
+
SNAPCTL_INPUT_ERROR)
|
|
426
|
+
if not self.http_port:
|
|
427
|
+
snapctl_error("Missing Ingress HTTP Port",
|
|
428
|
+
SNAPCTL_INPUT_ERROR)
|
|
419
429
|
if not self.http_port.isdigit():
|
|
420
|
-
|
|
421
|
-
|
|
430
|
+
snapctl_error("Ingress HTTP Port should be a number",
|
|
431
|
+
SNAPCTL_INPUT_ERROR)
|
|
422
432
|
# Check byosnap_profile path
|
|
423
|
-
|
|
424
|
-
if validation_response['error']:
|
|
425
|
-
response['msg'] = validation_response['message']
|
|
426
|
-
return response
|
|
427
|
-
|
|
428
|
-
# Send success
|
|
429
|
-
response['error'] = False
|
|
430
|
-
return response
|
|
433
|
+
self._validate_byosnap_profile()
|
|
431
434
|
|
|
432
435
|
# CRUD methods
|
|
433
|
-
|
|
436
|
+
|
|
437
|
+
def build(self) -> None:
|
|
434
438
|
"""
|
|
435
439
|
Build the image
|
|
436
440
|
1. Check Dependencies
|
|
437
441
|
2. Login to Snapser Registry
|
|
438
442
|
3. Build your snap
|
|
439
443
|
"""
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
return True
|
|
444
|
+
self._check_dependencies()
|
|
445
|
+
self._docker_login()
|
|
446
|
+
self._docker_build()
|
|
444
447
|
|
|
445
448
|
def push(self) -> bool:
|
|
446
449
|
"""
|
|
@@ -450,26 +453,29 @@ class ByoSnap:
|
|
|
450
453
|
3. Tag the snap
|
|
451
454
|
4. Push your snap
|
|
452
455
|
"""
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
456
|
+
self._check_dependencies()
|
|
457
|
+
self._docker_login()
|
|
458
|
+
self._docker_tag()
|
|
459
|
+
self._docker_push()
|
|
457
460
|
|
|
458
|
-
|
|
461
|
+
# Upper echelon commands
|
|
462
|
+
def upload_docs(self) -> None:
|
|
459
463
|
'''
|
|
460
|
-
Note this step is optional hence we
|
|
464
|
+
Note this step is optional hence we do not raise a typer.Exit
|
|
461
465
|
'''
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
466
|
+
progress = Progress(
|
|
467
|
+
SpinnerColumn(),
|
|
468
|
+
TextColumn("[progress.description]{task.description}"),
|
|
469
|
+
transient=True,
|
|
470
|
+
)
|
|
471
|
+
progress.start()
|
|
472
|
+
progress.add_task(
|
|
473
|
+
description='Uploading your BYOSnap Docs...', total=None)
|
|
474
|
+
try:
|
|
475
|
+
swagger_file = os.path.join(self.path, 'swagger.json')
|
|
476
|
+
readme_file = os.path.join(self.path, 'README.md')
|
|
477
|
+
if os.path.isfile(swagger_file):
|
|
478
|
+
# Push the swagger.json
|
|
473
479
|
try:
|
|
474
480
|
attachment_file = open(swagger_file, "rb")
|
|
475
481
|
url = (
|
|
@@ -484,25 +490,18 @@ class ByoSnap:
|
|
|
484
490
|
if test_res.ok:
|
|
485
491
|
success('Uploaded swagger.json')
|
|
486
492
|
else:
|
|
487
|
-
|
|
493
|
+
info('Unable to upload your swagger.json')
|
|
488
494
|
except RequestException as e:
|
|
489
495
|
info(
|
|
490
496
|
f'Exception: Unable to find swagger.json at {self.path} {e}'
|
|
491
497
|
)
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
498
|
+
else:
|
|
499
|
+
info(f'No swagger.json found at {self.path}'
|
|
500
|
+
'. Skipping swagger.json upload')
|
|
495
501
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
with Progress(
|
|
500
|
-
SpinnerColumn(),
|
|
501
|
-
TextColumn("[progress.description]{task.description}"),
|
|
502
|
-
transient=True,
|
|
503
|
-
) as progress:
|
|
504
|
-
progress.add_task(
|
|
505
|
-
description='Uploading your README...', total=None)
|
|
502
|
+
# Push the README.md
|
|
503
|
+
if os.path.isfile(readme_file):
|
|
504
|
+
# Push the swagger.json
|
|
506
505
|
try:
|
|
507
506
|
attachment_file = open(readme_file, "rb")
|
|
508
507
|
url = (
|
|
@@ -517,67 +516,71 @@ class ByoSnap:
|
|
|
517
516
|
if test_res.ok:
|
|
518
517
|
success('Uploaded README.md')
|
|
519
518
|
else:
|
|
520
|
-
|
|
519
|
+
info('Unable to upload your README.md')
|
|
521
520
|
except RequestException as e:
|
|
522
521
|
info(
|
|
523
|
-
f'Exception: Unable to find README.md at {self.path} {str(e)}'
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
)
|
|
529
|
-
|
|
522
|
+
f'Exception: Unable to find README.md at {self.path} {str(e)}')
|
|
523
|
+
else:
|
|
524
|
+
info(
|
|
525
|
+
f'No README.md found at {self.path}. Skipping README.md upload')
|
|
526
|
+
except RequestException as e:
|
|
527
|
+
info(f'Exception: Unable to upload your API Json {str(e)}')
|
|
528
|
+
finally:
|
|
529
|
+
progress.stop()
|
|
530
|
+
# snapctl_success('BYOSnap upload successful', no_exit=no_exit)
|
|
530
531
|
|
|
531
|
-
|
|
532
|
-
def create(self) -> bool:
|
|
532
|
+
def create(self) -> None:
|
|
533
533
|
"""
|
|
534
534
|
Creating a new snap
|
|
535
535
|
"""
|
|
536
|
-
|
|
536
|
+
progress = Progress(
|
|
537
537
|
SpinnerColumn(),
|
|
538
538
|
TextColumn("[progress.description]{task.description}"),
|
|
539
539
|
transient=True,
|
|
540
|
-
)
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
else:
|
|
573
|
-
error(
|
|
574
|
-
f'Server error: {json.dumps(response_json, indent=2)}'
|
|
540
|
+
)
|
|
541
|
+
progress.start()
|
|
542
|
+
progress.add_task(description='Creating your snap...', total=None)
|
|
543
|
+
try:
|
|
544
|
+
payload = {
|
|
545
|
+
"service_id": self.sid,
|
|
546
|
+
"name": self.name,
|
|
547
|
+
"description": self.desc,
|
|
548
|
+
"platform": self.platform_type,
|
|
549
|
+
"language": self.language,
|
|
550
|
+
}
|
|
551
|
+
res = requests.post(
|
|
552
|
+
f"{self.base_url}/v1/snapser-api/byosnaps",
|
|
553
|
+
json=payload, headers={'api-key': self.api_key},
|
|
554
|
+
timeout=SERVER_CALL_TIMEOUT
|
|
555
|
+
)
|
|
556
|
+
if res.ok:
|
|
557
|
+
snapctl_success('BYOSNAP create successful', progress)
|
|
558
|
+
response_json = res.json()
|
|
559
|
+
if "api_error_code" in response_json and "message" in response_json:
|
|
560
|
+
if response_json['api_error_code'] == HTTP_ERROR_SERVICE_VERSION_EXISTS:
|
|
561
|
+
snapctl_error(
|
|
562
|
+
f'BYOSnap {self.name} already exists. Please use a different name',
|
|
563
|
+
SNAPCTL_BYOSNAP_CREATE_DUPLICATE_NAME_ERROR,
|
|
564
|
+
progress
|
|
565
|
+
)
|
|
566
|
+
# elif response_json['api_error_code'] == HTTP_ERROR_TAG_NOT_AVAILABLE:
|
|
567
|
+
# error('Invalid tag. Please use the correct tag')
|
|
568
|
+
if response_json['api_error_code'] == HTTP_ERROR_ADD_ON_NOT_ENABLED:
|
|
569
|
+
snapctl_error(
|
|
570
|
+
'Missing Add-on. Please enable the add-on via the Snapser Web app.',
|
|
571
|
+
SNAPCTL_BYOSNAP_CREATE_PERMISSION_ERROR, progress
|
|
575
572
|
)
|
|
576
|
-
|
|
577
|
-
error
|
|
578
|
-
|
|
573
|
+
snapctl_error(
|
|
574
|
+
f'Server error: {json.dumps(response_json, indent=2)}',
|
|
575
|
+
SNAPCTL_BYOSNAP_CREATE_ERROR, progress)
|
|
576
|
+
except RequestException as e:
|
|
577
|
+
snapctl_error(
|
|
578
|
+
f"Exception: Unable to create your snap {e}",
|
|
579
|
+
SNAPCTL_BYOSNAP_CREATE_ERROR, progress)
|
|
580
|
+
snapctl_error('Failed to create snap',
|
|
581
|
+
SNAPCTL_BYOSNAP_CREATE_ERROR, progress)
|
|
579
582
|
|
|
580
|
-
def publish_image(self) ->
|
|
583
|
+
def publish_image(self) -> None:
|
|
581
584
|
"""
|
|
582
585
|
Publish the image
|
|
583
586
|
1. Check Dependencies
|
|
@@ -587,60 +590,64 @@ class ByoSnap:
|
|
|
587
590
|
5. Push the image
|
|
588
591
|
6. Upload swagger.json
|
|
589
592
|
"""
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
593
|
+
self._check_dependencies()
|
|
594
|
+
self._docker_login()
|
|
595
|
+
self._docker_build()
|
|
596
|
+
self._docker_tag()
|
|
597
|
+
self._docker_push()
|
|
598
|
+
self.upload_docs()
|
|
599
|
+
snapctl_success('BYOSNAP publish successful')
|
|
595
600
|
|
|
596
|
-
def publish_version(self) ->
|
|
601
|
+
def publish_version(self) -> None:
|
|
597
602
|
"""
|
|
598
603
|
Publish the version
|
|
599
604
|
"""
|
|
600
|
-
|
|
605
|
+
progress = Progress(
|
|
601
606
|
SpinnerColumn(),
|
|
602
607
|
TextColumn("[progress.description]{task.description}"),
|
|
603
608
|
transient=True,
|
|
604
|
-
)
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
else:
|
|
639
|
-
error(
|
|
640
|
-
f'Server error: {json.dumps(response_json, indent=2)}'
|
|
609
|
+
)
|
|
610
|
+
progress.start()
|
|
611
|
+
progress.add_task(
|
|
612
|
+
description='Publishing your snap...', total=None)
|
|
613
|
+
try:
|
|
614
|
+
profile_data = {}
|
|
615
|
+
profile_data['dev_template'] = None
|
|
616
|
+
profile_data['stage_template'] = None
|
|
617
|
+
profile_data['prod_template'] = None
|
|
618
|
+
with open(self.byosnap_profile, 'rb') as file:
|
|
619
|
+
profile_data = json.load(file)
|
|
620
|
+
payload = {
|
|
621
|
+
"version": self.version,
|
|
622
|
+
"image_tag": self.input_tag,
|
|
623
|
+
"base_url": f"{self.prefix}/{self.sid}",
|
|
624
|
+
"http_port": self.http_port,
|
|
625
|
+
"dev_template": profile_data['dev_template'],
|
|
626
|
+
"stage_template": profile_data['stage_template'],
|
|
627
|
+
"prod_template": profile_data['prod_template']
|
|
628
|
+
}
|
|
629
|
+
res = requests.post(
|
|
630
|
+
f"{self.base_url}/v1/snapser-api/byosnaps/{self.sid}/versions",
|
|
631
|
+
json=payload, headers={'api-key': self.api_key},
|
|
632
|
+
timeout=SERVER_CALL_TIMEOUT
|
|
633
|
+
)
|
|
634
|
+
if res.ok:
|
|
635
|
+
snapctl_success('BYOSNAP publish version successful', progress)
|
|
636
|
+
response_json = res.json()
|
|
637
|
+
if "api_error_code" in response_json:
|
|
638
|
+
if response_json['api_error_code'] == HTTP_ERROR_SERVICE_VERSION_EXISTS:
|
|
639
|
+
snapctl_error(
|
|
640
|
+
'Version already exists. Please update your version and try again',
|
|
641
|
+
SNAPCTL_BYOSNAP_PUBLISH_IMAGE_DUPLICATE_TAG_ERROR,
|
|
642
|
+
progress
|
|
641
643
|
)
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
644
|
+
if response_json['api_error_code'] == HTTP_ERROR_TAG_NOT_AVAILABLE:
|
|
645
|
+
snapctl_error('Invalid tag. Please use the correct tag',
|
|
646
|
+
SNAPCTL_BYOSNAP_PUBLISH_VERSION_DUPLICATE_TAG_ERROR, progress)
|
|
647
|
+
snapctl_error(f'Server error: {json.dumps(response_json, indent=2)}',
|
|
648
|
+
SNAPCTL_BYOSNAP_PUBLISH_VERSION_ERROR, progress)
|
|
649
|
+
except RequestException as e:
|
|
650
|
+
snapctl_error(f'Exception: Unable to publish a version for your snap {e}',
|
|
651
|
+
SNAPCTL_BYOSNAP_PUBLISH_VERSION_ERROR, progress)
|
|
652
|
+
snapctl_error('Failed to publish version',
|
|
653
|
+
SNAPCTL_BYOSNAP_PUBLISH_VERSION_ERROR, progress)
|