snapctl 0.43.2__py3-none-any.whl → 0.46.0__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 +37 -18
- snapctl/commands/byosnap.py +623 -222
- snapctl/commands/generate.py +3 -71
- snapctl/commands/release_notes.py +65 -0
- snapctl/commands/snapend.py +91 -76
- snapctl/config/constants.py +11 -9
- snapctl/config/hashes.py +18 -25
- snapctl/data/profiles/snapser-byosnap-profile.json +62 -0
- snapctl/data/profiles/snapser-byosnap-profile.yaml +54 -0
- snapctl/data/releases/beta-0.46.0.mdx +55 -0
- snapctl/main.py +81 -67
- {snapctl-0.43.2.dist-info → snapctl-0.46.0.dist-info}/METADATA +199 -98
- snapctl-0.46.0.dist-info/RECORD +27 -0
- snapctl-0.43.2.dist-info/RECORD +0 -23
- {snapctl-0.43.2.dist-info → snapctl-0.46.0.dist-info}/LICENSE +0 -0
- {snapctl-0.43.2.dist-info → snapctl-0.46.0.dist-info}/WHEEL +0 -0
- {snapctl-0.43.2.dist-info → snapctl-0.46.0.dist-info}/entry_points.txt +0 -0
snapctl/commands/byosnap.py
CHANGED
|
@@ -13,6 +13,7 @@ from sys import platform
|
|
|
13
13
|
from typing import Union, List
|
|
14
14
|
import requests
|
|
15
15
|
from requests.exceptions import RequestException
|
|
16
|
+
import yaml
|
|
16
17
|
|
|
17
18
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
18
19
|
from snapctl.commands.snapend import Snapend
|
|
@@ -27,7 +28,8 @@ from snapctl.config.constants import HTTP_ERROR_SERVICE_VERSION_EXISTS, \
|
|
|
27
28
|
SNAPCTL_BYOSNAP_PUBLISH_VERSION_ERROR, HTTP_ERROR_SERVICE_IN_USE, \
|
|
28
29
|
SNAPCTL_BYOSNAP_UPDATE_VERSION_ERROR, SNAPCTL_BYOSNAP_UPDATE_VERSION_SERVICE_IN_USE_ERROR, \
|
|
29
30
|
SNAPCTL_BYOSNAP_UPDATE_VERSION_TAG_ERROR, SNAPCTL_BYOSNAP_NOT_FOUND, \
|
|
30
|
-
HTTP_ERROR_RESOURCE_NOT_FOUND, SNAPCTL_BYOSNAP_PUBLISH_ERROR
|
|
31
|
+
HTTP_ERROR_RESOURCE_NOT_FOUND, SNAPCTL_BYOSNAP_PUBLISH_ERROR, \
|
|
32
|
+
SNAPCTL_BYOSNAP_GENERATE_PROFILE_ERROR
|
|
31
33
|
from snapctl.utils.echo import info, warning, success
|
|
32
34
|
from snapctl.utils.helper import get_composite_token, snapctl_error, snapctl_success, \
|
|
33
35
|
check_dockerfile_architecture
|
|
@@ -38,11 +40,21 @@ class ByoSnap:
|
|
|
38
40
|
CLI commands exposed for a BYOSnap
|
|
39
41
|
"""
|
|
40
42
|
ID_PREFIX = 'byosnap-'
|
|
43
|
+
# These are active today
|
|
41
44
|
SUBCOMMANDS = [
|
|
42
|
-
'
|
|
43
|
-
'publish', '
|
|
45
|
+
'publish', 'sync', 'upload-docs', 'generate-profile', 'validate-profile',
|
|
46
|
+
'create', 'publish-image', 'publish-version', 'update-version',
|
|
44
47
|
]
|
|
45
|
-
|
|
48
|
+
# These are the real commands that we want to show in the help text
|
|
49
|
+
SHOW_SUBCOMMANDS = ['publish', 'sync', 'upload-docs',
|
|
50
|
+
'generate-profile', 'validate-profile']
|
|
51
|
+
# These are the commands that we want to deprecate
|
|
52
|
+
TO_DEPRECATE_SUBCOMMANDS = [
|
|
53
|
+
'create', 'publish-image', 'publish-version', 'update-version']
|
|
54
|
+
DEFAULT_PROFILE_NAME_JSON = 'snapser-byosnap-profile.json'
|
|
55
|
+
DEFAULT_PROFILE_NAME_YAML = 'snapser-byosnap-profile.yaml'
|
|
56
|
+
PROFILE_FILES_PATH = 'snapctl/data/profiles/'
|
|
57
|
+
PROFILE_FORMATS = ['json', 'yaml', 'yml']
|
|
46
58
|
PLATFORMS = ['linux/arm64', 'linux/amd64']
|
|
47
59
|
LANGUAGES = ['go', 'node', 'python', 'java', 'csharp', 'cpp', 'rust',
|
|
48
60
|
'ruby', 'php', 'perl', 'clojure', 'lua', 'ts', 'js', 'kotlin', 'c']
|
|
@@ -55,32 +67,26 @@ class ByoSnap:
|
|
|
55
67
|
MAX_MIN_REPLICAS = 4
|
|
56
68
|
|
|
57
69
|
def __init__(
|
|
58
|
-
self, *, subcommand: str, base_url: str, api_key: Union[str, None],
|
|
70
|
+
self, *, subcommand: str, base_url: str, api_key: Union[str, None], byosnap_id: str,
|
|
59
71
|
name: Union[str, None] = None, desc: Union[str, None] = None,
|
|
60
72
|
platform_type: Union[str, None] = None, language: Union[str, None] = None,
|
|
61
73
|
tag: Union[str, None] = None, path: Union[str, None] = None,
|
|
62
|
-
resources_path: Union[str, None] = None,
|
|
74
|
+
resources_path: Union[str, None] = None, docker_filename: Union[str, None] = None,
|
|
63
75
|
version: Union[str, None] = None, skip_build: bool = False,
|
|
64
76
|
snapend_id: Union[str, None] = None, blocking: bool = False,
|
|
65
|
-
|
|
77
|
+
profile_filename: Union[str, None] = None,
|
|
78
|
+
out_path: Union[str, None] = None
|
|
66
79
|
) -> None:
|
|
67
80
|
# Set the BASE variables
|
|
68
81
|
self.subcommand: str = subcommand
|
|
69
82
|
self.base_url: str = base_url
|
|
70
83
|
self.api_key: Union[str, None] = api_key
|
|
71
|
-
self.
|
|
72
|
-
self.token: Union[str, None] = None
|
|
73
|
-
self.token_parts: Union[list, None] = None
|
|
74
|
-
self.byosnap_profile: Union[str, None] = None
|
|
75
|
-
if subcommand not in ['create', 'publish']:
|
|
76
|
-
# Create does not need the token
|
|
77
|
-
# Publish handles this in the publish-image subcommand
|
|
78
|
-
self._setup_token_and_token_parts(base_url, api_key, sid)
|
|
84
|
+
self.byosnap_id: str = byosnap_id
|
|
79
85
|
self.tag: Union[str, None] = tag
|
|
80
86
|
self.path: Union[str, None] = path
|
|
81
87
|
self.resources_path: Union[str, None] = resources_path
|
|
82
|
-
self.
|
|
83
|
-
self.
|
|
88
|
+
self.docker_filename: str = docker_filename
|
|
89
|
+
self.profile_filename: Union[str, None] = profile_filename
|
|
84
90
|
self.version: Union[str, None] = version
|
|
85
91
|
self.name: Union[str, None] = name
|
|
86
92
|
self.desc: Union[str, None] = desc
|
|
@@ -89,16 +95,21 @@ class ByoSnap:
|
|
|
89
95
|
self.snapend_id: Union[str, None] = snapend_id
|
|
90
96
|
self.skip_build: bool = skip_build
|
|
91
97
|
self.blocking: bool = blocking
|
|
92
|
-
|
|
98
|
+
self.out_path: Union[str, None] = out_path
|
|
99
|
+
# Values below will be overridden
|
|
100
|
+
self.token: Union[str, None] = None
|
|
101
|
+
self.token_parts: Union[list, None] = None
|
|
102
|
+
self.profile_path: Union[str, None] = None
|
|
103
|
+
self.profile_data: Union[dict, None] = None
|
|
104
|
+
# These variables are here because of backward compatibility
|
|
105
|
+
# We now takes these inputs from the BYOSnap profile
|
|
93
106
|
self.prefix: Union[str, None] = None
|
|
94
107
|
self.ingress_external_port: Union[dict, None] = None
|
|
95
108
|
self.ingress_internal_ports: Union[list, None] = None
|
|
96
109
|
self.readiness_path: Union[str, None] = None
|
|
97
110
|
self.readiness_delay: Union[str, None] = None
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
# Validate the input
|
|
101
|
-
self.validate_input()
|
|
111
|
+
# Setup and Validate the input
|
|
112
|
+
self.setup_and_validate_input()
|
|
102
113
|
|
|
103
114
|
# Protected methods
|
|
104
115
|
@staticmethod
|
|
@@ -123,32 +134,227 @@ class ByoSnap:
|
|
|
123
134
|
pass
|
|
124
135
|
return None
|
|
125
136
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
'''
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
profile_data = None
|
|
142
|
-
with open(self.byosnap_profile, 'rb') as file:
|
|
143
|
-
try:
|
|
144
|
-
profile_data = json.load(file)
|
|
145
|
-
except json.JSONDecodeError:
|
|
146
|
-
pass
|
|
147
|
-
if not profile_data:
|
|
137
|
+
@staticmethod
|
|
138
|
+
def _validate_byosnap_profile_data(profile_data) -> None:
|
|
139
|
+
# Check for the parent fields
|
|
140
|
+
for field in ['name', 'description', 'platform', 'language', 'prefix',
|
|
141
|
+
'readiness_probe_config', 'dev_template', 'stage_template', 'prod_template']:
|
|
142
|
+
if field not in profile_data:
|
|
143
|
+
snapctl_error(
|
|
144
|
+
message=f'BYOSnap profile requires {field} field. ' +
|
|
145
|
+
'Please use the following command: ' +
|
|
146
|
+
'`snapctl byosnap generate-profile --out-path $output_path` ' +
|
|
147
|
+
'to generate a new profile',
|
|
148
|
+
code=SNAPCTL_INPUT_ERROR
|
|
149
|
+
)
|
|
150
|
+
# Check for backward compatible fields
|
|
151
|
+
if 'http_port' not in profile_data and 'ingress' not in profile_data:
|
|
148
152
|
snapctl_error(
|
|
149
|
-
message='
|
|
153
|
+
message='BYOSnap profile requires an ingress field with external_port and ' +
|
|
154
|
+
'internal_port as children. Note: http_port is going to be deprecated soon. ' +
|
|
155
|
+
'Please use the following command: ' +
|
|
156
|
+
'`snapctl byosnap generate-profile --out-path $output_path` ' +
|
|
157
|
+
'to generate a new profile',
|
|
158
|
+
code=SNAPCTL_INPUT_ERROR
|
|
159
|
+
)
|
|
160
|
+
# Name Check
|
|
161
|
+
if profile_data['name'] is None or profile_data['name'].strip() == '':
|
|
162
|
+
snapctl_error(
|
|
163
|
+
message='BYOSnap profile requires a non empty name value. ' +
|
|
164
|
+
'Please use the following command: ' +
|
|
165
|
+
'`snapctl byosnap generate-profile --out-path $output_path` ' +
|
|
166
|
+
'to generate a new profile',
|
|
150
167
|
code=SNAPCTL_INPUT_ERROR
|
|
151
168
|
)
|
|
169
|
+
# Description Check
|
|
170
|
+
if profile_data['description'] is None or profile_data['description'].strip() == '':
|
|
171
|
+
snapctl_error(
|
|
172
|
+
message='BYOSnap profile requires a non empty description value. ' +
|
|
173
|
+
'Please use the following command: ' +
|
|
174
|
+
'`snapctl byosnap generate-profile --out-path $output_path` ' +
|
|
175
|
+
'to generate a new profile',
|
|
176
|
+
code=SNAPCTL_INPUT_ERROR
|
|
177
|
+
)
|
|
178
|
+
# Platform Check
|
|
179
|
+
if profile_data['platform'] is None or \
|
|
180
|
+
profile_data['platform'].strip() not in ByoSnap.PLATFORMS:
|
|
181
|
+
snapctl_error(
|
|
182
|
+
message='Invalid platform value in BYOSnap profile. Valid values are ' +
|
|
183
|
+
f'{", ".join(map(str, ByoSnap.PLATFORMS))}.',
|
|
184
|
+
code=SNAPCTL_INPUT_ERROR
|
|
185
|
+
)
|
|
186
|
+
# Language Check
|
|
187
|
+
if profile_data['language'] is None or profile_data['language'].strip() == '':
|
|
188
|
+
snapctl_error(
|
|
189
|
+
message='BYOSnap profile requires a non empty language value. ' +
|
|
190
|
+
'Please use the following command: ' +
|
|
191
|
+
'`snapctl byosnap generate-profile --out-path $output_path` ' +
|
|
192
|
+
'to generate a new profile',
|
|
193
|
+
code=SNAPCTL_INPUT_ERROR
|
|
194
|
+
)
|
|
195
|
+
# Prefix Checks
|
|
196
|
+
if profile_data['prefix'] is None or profile_data['prefix'].strip() == '':
|
|
197
|
+
snapctl_error(
|
|
198
|
+
message='BYOSnap profile requires a non empty prefix value. ' +
|
|
199
|
+
'Please use the following command: ' +
|
|
200
|
+
'`snapctl byosnap generate-profile --out-path $output_path` ' +
|
|
201
|
+
'to generate a new profile',
|
|
202
|
+
code=SNAPCTL_INPUT_ERROR
|
|
203
|
+
)
|
|
204
|
+
if not profile_data['prefix'].strip().startswith('/') or \
|
|
205
|
+
profile_data['prefix'].strip().endswith('/') or \
|
|
206
|
+
profile_data['prefix'].strip().count('/') > 1:
|
|
207
|
+
snapctl_error(
|
|
208
|
+
message='Invalid prefix value in BYOSnap profile. ' +
|
|
209
|
+
'Prefix should start with a forward slash (/) and should contain exactly one ' +
|
|
210
|
+
'path segment.',
|
|
211
|
+
code=SNAPCTL_INPUT_ERROR
|
|
212
|
+
)
|
|
213
|
+
# HTTP Port and Ingress Checks
|
|
214
|
+
if 'http_port' in profile_data:
|
|
215
|
+
if profile_data['http_port'] is None or \
|
|
216
|
+
not isinstance(profile_data['http_port'], int):
|
|
217
|
+
snapctl_error(
|
|
218
|
+
message='Invalid http_port value in BYOSnap profile. ' +
|
|
219
|
+
'HTTP port should be a number.',
|
|
220
|
+
code=SNAPCTL_INPUT_ERROR
|
|
221
|
+
)
|
|
222
|
+
warning('http_port is deprecated. Please use ingress.external_port. ' +
|
|
223
|
+
'You can generate a new BYOSnap profile via the `generate` command.')
|
|
224
|
+
# Ingress Checks
|
|
225
|
+
if 'ingress' in profile_data:
|
|
226
|
+
if profile_data['ingress'] is None:
|
|
227
|
+
snapctl_error(
|
|
228
|
+
message='BYOSnap profile requires an ingress field with external_port and ' +
|
|
229
|
+
'internal_port as children. Please use the following command: ' +
|
|
230
|
+
'`snapctl byosnap generate-profile --out-path $output_path` ' +
|
|
231
|
+
'to generate a new profile',
|
|
232
|
+
code=SNAPCTL_INPUT_ERROR
|
|
233
|
+
)
|
|
234
|
+
if 'external_port' not in profile_data['ingress']:
|
|
235
|
+
snapctl_error(
|
|
236
|
+
message='BYOSnap profile requires an ingress.external_port field. ' +
|
|
237
|
+
'Please use the following command: ' +
|
|
238
|
+
'`snapctl byosnap generate-profile --out-path $output_path` ' +
|
|
239
|
+
'to generate a new profile',
|
|
240
|
+
code=SNAPCTL_INPUT_ERROR
|
|
241
|
+
)
|
|
242
|
+
if 'name' not in profile_data['ingress']['external_port'] or \
|
|
243
|
+
profile_data['ingress']['external_port']['name'] != 'http':
|
|
244
|
+
snapctl_error(
|
|
245
|
+
message='Invalid Ingress external_port value in BYOSnap profile. ' +
|
|
246
|
+
'External port should have a name of http and a number port value.',
|
|
247
|
+
code=SNAPCTL_INPUT_ERROR
|
|
248
|
+
)
|
|
249
|
+
if 'port' not in profile_data['ingress']['external_port'] or \
|
|
250
|
+
profile_data['ingress']['external_port']['port'] is None or \
|
|
251
|
+
not isinstance(profile_data['ingress']['external_port']['port'], int):
|
|
252
|
+
snapctl_error(
|
|
253
|
+
message='Invalid Ingress external_port value in BYOSnap profile. ' +
|
|
254
|
+
'External port should have a name of http and a number port value.',
|
|
255
|
+
code=SNAPCTL_INPUT_ERROR
|
|
256
|
+
)
|
|
257
|
+
if 'internal_ports' not in profile_data['ingress'] or \
|
|
258
|
+
not isinstance(profile_data['ingress']['internal_ports'], list):
|
|
259
|
+
snapctl_error(
|
|
260
|
+
message='Invalid Ingress internal_port value in BYOSnap profile. ' +
|
|
261
|
+
'Internal port should be an empty list or a list of objects with name and ' +
|
|
262
|
+
'port values.',
|
|
263
|
+
code=SNAPCTL_INPUT_ERROR
|
|
264
|
+
)
|
|
265
|
+
duplicate_name = {}
|
|
266
|
+
duplicate_port = {}
|
|
267
|
+
index = 0
|
|
268
|
+
for internal_port_obj in profile_data['ingress']['internal_ports']:
|
|
269
|
+
index += 1
|
|
270
|
+
if 'name' not in internal_port_obj or 'port' not in internal_port_obj:
|
|
271
|
+
snapctl_error(
|
|
272
|
+
message='Invalid Ingress internal_port value in BYOSnap profile. ' +
|
|
273
|
+
'Internal port should be an object with name and port values. ' +
|
|
274
|
+
f"Check the internal port number #{index}.",
|
|
275
|
+
code=SNAPCTL_INPUT_ERROR
|
|
276
|
+
)
|
|
277
|
+
if internal_port_obj['name'] is None or internal_port_obj['name'].strip() == '':
|
|
278
|
+
snapctl_error(
|
|
279
|
+
message='Invalid Ingress internal_port value in BYOSnap profile. ' +
|
|
280
|
+
'Internal port name should not be empty. ' +
|
|
281
|
+
f"Check the internal port number #{index}.",
|
|
282
|
+
code=SNAPCTL_INPUT_ERROR
|
|
283
|
+
)
|
|
284
|
+
if internal_port_obj['port'] is None or \
|
|
285
|
+
not isinstance(internal_port_obj['port'], int):
|
|
286
|
+
snapctl_error(
|
|
287
|
+
message='Invalid Ingress internal_port value in BYOSnap profile. ' +
|
|
288
|
+
'Internal port port should be a number. ' +
|
|
289
|
+
f"Check the internal port number #{index}.",
|
|
290
|
+
code=SNAPCTL_INPUT_ERROR
|
|
291
|
+
)
|
|
292
|
+
# Confirm the name does not collide with the external port
|
|
293
|
+
if internal_port_obj['name'] == profile_data['ingress']['external_port']['name']:
|
|
294
|
+
snapctl_error("Internal port name should not be the same as " +
|
|
295
|
+
"the external port name. " +
|
|
296
|
+
f"Check the internal port number #{index}.",
|
|
297
|
+
SNAPCTL_INPUT_ERROR)
|
|
298
|
+
if internal_port_obj['port'] == profile_data['ingress']['external_port']['port']:
|
|
299
|
+
snapctl_error("Internal port number should not be the same as " +
|
|
300
|
+
"the external port number. " +
|
|
301
|
+
f"Check the internal port number #{index}.",
|
|
302
|
+
SNAPCTL_INPUT_ERROR)
|
|
303
|
+
if internal_port_obj['name'] in duplicate_name:
|
|
304
|
+
snapctl_error("Duplicate internal port name. " +
|
|
305
|
+
f"Check the internal port number #{index}.",
|
|
306
|
+
SNAPCTL_INPUT_ERROR)
|
|
307
|
+
if internal_port_obj['port'] in duplicate_port:
|
|
308
|
+
snapctl_error("Duplicate internal port number. " +
|
|
309
|
+
f"Check the internal port number #{index}.",
|
|
310
|
+
SNAPCTL_INPUT_ERROR)
|
|
311
|
+
duplicate_name[internal_port_obj['name']] = True
|
|
312
|
+
duplicate_port[internal_port_obj['port']] = True
|
|
313
|
+
# Readiness Probe Checks
|
|
314
|
+
if 'readiness_probe_config' not in profile_data:
|
|
315
|
+
snapctl_error(
|
|
316
|
+
message='BYOSnap profile requires a readiness_probe_config field. ' +
|
|
317
|
+
'Please use the following command: ' +
|
|
318
|
+
'`snapctl byosnap generate-profile --out-path $output_path` ' +
|
|
319
|
+
'to generate a new profile',
|
|
320
|
+
code=SNAPCTL_INPUT_ERROR
|
|
321
|
+
)
|
|
322
|
+
if 'initial_delay_seconds' not in profile_data['readiness_probe_config'] or \
|
|
323
|
+
'path' not in profile_data['readiness_probe_config']:
|
|
324
|
+
snapctl_error(
|
|
325
|
+
message='Invalid readiness_probe_config value in BYOSnap profile. ' +
|
|
326
|
+
'Readiness probe config should have an initial_delay_seconds and path value. ' +
|
|
327
|
+
'Set both to null if not required.',
|
|
328
|
+
code=SNAPCTL_INPUT_ERROR
|
|
329
|
+
)
|
|
330
|
+
if (profile_data['readiness_probe_config']['initial_delay_seconds'] is None and
|
|
331
|
+
profile_data['readiness_probe_config']['path'] is not None) or \
|
|
332
|
+
(profile_data['readiness_probe_config']['initial_delay_seconds'] is not None and
|
|
333
|
+
profile_data['readiness_probe_config']['path'] is None):
|
|
334
|
+
snapctl_error(
|
|
335
|
+
message='Invalid readiness_probe_config value in BYOSnap profile. ' +
|
|
336
|
+
'Readiness probe config should have both initial_delay_seconds and path values. ' +
|
|
337
|
+
'One of them cannot be null. However, set both to null if not required.',
|
|
338
|
+
code=SNAPCTL_INPUT_ERROR
|
|
339
|
+
)
|
|
340
|
+
if profile_data['readiness_probe_config']['path'] is not None is not None:
|
|
341
|
+
if profile_data['readiness_probe_config']['path'].strip() == '':
|
|
342
|
+
snapctl_error(
|
|
343
|
+
"Invalid readiness_probe_config.path value. Readiness path cannot be empty",
|
|
344
|
+
SNAPCTL_INPUT_ERROR)
|
|
345
|
+
if not profile_data['readiness_probe_config']['path'].strip().startswith('/'):
|
|
346
|
+
snapctl_error(
|
|
347
|
+
"Invalid readiness_probe_config.path value. Readiness path has to start with /",
|
|
348
|
+
SNAPCTL_INPUT_ERROR)
|
|
349
|
+
if profile_data['readiness_probe_config']['initial_delay_seconds'] is not None:
|
|
350
|
+
if not isinstance(profile_data['readiness_probe_config']['initial_delay_seconds'], int) or \
|
|
351
|
+
profile_data['readiness_probe_config']['initial_delay_seconds'] < 0 or \
|
|
352
|
+
profile_data['readiness_probe_config']['initial_delay_seconds'] > ByoSnap.MAX_READINESS_TIMEOUT:
|
|
353
|
+
snapctl_error(
|
|
354
|
+
"Invalid readiness_probe_config.path value. " +
|
|
355
|
+
"Readiness delay should be between 0 " +
|
|
356
|
+
f"and {ByoSnap.MAX_READINESS_TIMEOUT}", SNAPCTL_INPUT_ERROR)
|
|
357
|
+
# Template Object Checks
|
|
152
358
|
if 'dev_template' not in profile_data or \
|
|
153
359
|
'stage_template' not in profile_data or \
|
|
154
360
|
'prod_template' not in profile_data:
|
|
@@ -157,171 +363,224 @@ class ByoSnap:
|
|
|
157
363
|
code=SNAPCTL_INPUT_ERROR
|
|
158
364
|
)
|
|
159
365
|
for profile in ['dev_template', 'stage_template', 'prod_template']:
|
|
160
|
-
#
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
366
|
+
# IMPORTANT: Not checking for in min_replicas for backward compatibility
|
|
367
|
+
for field in ['cpu', 'memory', 'cmd', 'args', 'env_params']:
|
|
368
|
+
if field not in profile_data[profile]:
|
|
369
|
+
snapctl_error(
|
|
370
|
+
message='Invalid BYOSnap profile JSON. ' +
|
|
371
|
+
f'{profile} requires cpu, memory, min_replicas, cmd, args, and ' +
|
|
372
|
+
'env_params fields.',
|
|
373
|
+
code=SNAPCTL_INPUT_ERROR
|
|
374
|
+
)
|
|
375
|
+
if profile_data[profile]['cpu'] is None or \
|
|
376
|
+
profile_data[profile]['cpu'] not in ByoSnap.VALID_CPU_MARKS:
|
|
171
377
|
snapctl_error(
|
|
172
378
|
message='Invalid CPU value in BYOSnap profile. Valid values are' +
|
|
173
|
-
f'{", ".join(map(str, ByoSnap.VALID_CPU_MARKS))}',
|
|
379
|
+
f'{", ".join(map(str, ByoSnap.VALID_CPU_MARKS))}.',
|
|
174
380
|
code=SNAPCTL_INPUT_ERROR
|
|
175
381
|
)
|
|
176
|
-
if profile_data[profile]['memory']
|
|
382
|
+
if profile_data[profile]['memory'] is None or \
|
|
383
|
+
profile_data[profile]['memory'] not in ByoSnap.VALID_MEMORY_MARKS:
|
|
177
384
|
snapctl_error(
|
|
178
385
|
message='Invalid Memory value in BYOSnap profile. Valid values are ' +
|
|
179
|
-
f'{", ".join(map(str, ByoSnap.VALID_MEMORY_MARKS))}',
|
|
386
|
+
f'{", ".join(map(str, ByoSnap.VALID_MEMORY_MARKS))}.',
|
|
180
387
|
code=SNAPCTL_INPUT_ERROR
|
|
181
388
|
)
|
|
182
389
|
if 'min_replicas' in profile_data[profile] and \
|
|
390
|
+
profile_data[profile]['min_replicas'] is not None and \
|
|
183
391
|
(not isinstance(profile_data[profile]['min_replicas'], int) or
|
|
184
|
-
|
|
185
|
-
|
|
392
|
+
profile_data[profile]['min_replicas'] < 0 or
|
|
393
|
+
profile_data[profile]['min_replicas'] > ByoSnap.MAX_MIN_REPLICAS):
|
|
186
394
|
snapctl_error(
|
|
187
395
|
message='Invalid Min Replicas value in BYOSnap profile. ' +
|
|
188
396
|
'Minimum replicas should be between 0 and ' +
|
|
189
397
|
f'{ByoSnap.MAX_MIN_REPLICAS}',
|
|
190
398
|
code=SNAPCTL_INPUT_ERROR
|
|
191
399
|
)
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
'
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
400
|
+
if 'min_replicas' in profile_data[profile] and \
|
|
401
|
+
profile_data[profile]['min_replicas'] is not None and \
|
|
402
|
+
isinstance(profile_data[profile]['min_replicas'], int) and \
|
|
403
|
+
profile_data[profile]['min_replicas'] > 1 and \
|
|
404
|
+
(profile == 'dev_template' or profile == 'stage_template'):
|
|
405
|
+
snapctl_error(
|
|
406
|
+
message='Invalid Min Replicas value in BYOSnap profile. ' +
|
|
407
|
+
'Minimum replicas should be 1 for dev and stage templates.',
|
|
408
|
+
code=SNAPCTL_INPUT_ERROR
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
if profile_data[profile]['cmd'] is None:
|
|
412
|
+
snapctl_error(
|
|
413
|
+
message='Invalid CMD value in BYOSnap profile. CMD should not be an ' +
|
|
414
|
+
'empty string or the command you want to run in the container.',
|
|
415
|
+
code=SNAPCTL_INPUT_ERROR
|
|
416
|
+
)
|
|
417
|
+
if profile_data[profile]['args'] is None or \
|
|
418
|
+
not isinstance(profile_data[profile]['args'], list):
|
|
419
|
+
snapctl_error(
|
|
420
|
+
message='Invalid ARGS value in BYOSnap profile. ARGS should be a ' +
|
|
421
|
+
'list of arguments or an empty list if no arguments are required.',
|
|
422
|
+
code=SNAPCTL_INPUT_ERROR
|
|
423
|
+
)
|
|
424
|
+
if profile_data[profile]['env_params'] is None or \
|
|
425
|
+
not isinstance(profile_data[profile]['env_params'], list):
|
|
426
|
+
snapctl_error(
|
|
427
|
+
message='Invalid env_params value in BYOSnap profile. env_params should be a ' +
|
|
428
|
+
'list of environment variables as a dict with key and value attribute. ' +
|
|
429
|
+
'It can be an empty list if no environment variables are required.',
|
|
430
|
+
code=SNAPCTL_INPUT_ERROR
|
|
431
|
+
)
|
|
432
|
+
env_index = 0
|
|
433
|
+
for env_param in profile_data[profile]['env_params']:
|
|
434
|
+
env_index += 1
|
|
435
|
+
if 'key' not in env_param or 'value' not in env_param:
|
|
436
|
+
snapctl_error(
|
|
437
|
+
message='Invalid env_params value in BYOSnap profile. env_params should ' +
|
|
438
|
+
'be a list of environment variables as a dict with key and value ' +
|
|
439
|
+
'attribute. It can be an empty list if no environment variables ' +
|
|
440
|
+
'are required. ' +
|
|
441
|
+
f"Check the entry {profile}.env_params #{env_index}.",
|
|
442
|
+
code=SNAPCTL_INPUT_ERROR
|
|
443
|
+
)
|
|
444
|
+
if env_param['key'] is None or env_param['key'].strip() == '':
|
|
445
|
+
snapctl_error(
|
|
446
|
+
message='Invalid env_params value in BYOSnap profile. env_params key ' +
|
|
447
|
+
'should not be empty. ' +
|
|
448
|
+
f"Check the key entry at {
|
|
449
|
+
profile}.env_params #{env_index}.",
|
|
450
|
+
code=SNAPCTL_INPUT_ERROR
|
|
451
|
+
)
|
|
452
|
+
if env_param['value'] is None or env_param['value'].strip() == '':
|
|
453
|
+
snapctl_error(
|
|
454
|
+
message='Invalid env_params value in BYOSnap profile. env_params value ' +
|
|
455
|
+
'should not be empty. ' +
|
|
456
|
+
f"Check the value entry at {
|
|
457
|
+
profile}.env_params #{env_index}.",
|
|
458
|
+
code=SNAPCTL_INPUT_ERROR
|
|
459
|
+
)
|
|
460
|
+
return profile_data
|
|
461
|
+
|
|
462
|
+
@staticmethod
|
|
463
|
+
def _validate_byosnap_id(byosnap_id: str) -> None:
|
|
464
|
+
if not byosnap_id.startswith(ByoSnap.ID_PREFIX):
|
|
199
465
|
snapctl_error(
|
|
200
|
-
message=
|
|
201
|
-
|
|
202
|
-
'Please use the following command: ' +
|
|
203
|
-
'`snapctl generate profile --category byosnap --out-path $output_path` ' +
|
|
204
|
-
'to generate a new profile',
|
|
466
|
+
message="Invalid Snap ID. Valid Snap IDs start with " +
|
|
467
|
+
f"{ByoSnap.ID_PREFIX}.",
|
|
205
468
|
code=SNAPCTL_INPUT_ERROR
|
|
206
469
|
)
|
|
207
|
-
|
|
470
|
+
if len(byosnap_id) > ByoSnap.SID_CHARACTER_LIMIT:
|
|
471
|
+
snapctl_error(
|
|
472
|
+
message="Invalid Snap ID. Snap ID should be less than " +
|
|
473
|
+
f"{ByoSnap.SID_CHARACTER_LIMIT} characters",
|
|
474
|
+
code=SNAPCTL_INPUT_ERROR
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
@staticmethod
|
|
478
|
+
def _handle_output_file(input_filepath, output_filepath) -> bool:
|
|
479
|
+
file_written = False
|
|
480
|
+
with open(input_filepath, 'r') as in_file, open(output_filepath, 'w') as outfile:
|
|
481
|
+
for line in in_file:
|
|
482
|
+
outfile.write(line)
|
|
483
|
+
file_written = True
|
|
484
|
+
return file_written
|
|
485
|
+
|
|
486
|
+
def _get_profile_contents(self) -> dict:
|
|
487
|
+
"""
|
|
488
|
+
Get the BYOSNap profile contents
|
|
489
|
+
based on if the user has a YAML or JSON file
|
|
490
|
+
"""
|
|
491
|
+
profile_contents = {}
|
|
492
|
+
with open(self.profile_path, 'rb') as file:
|
|
493
|
+
try:
|
|
494
|
+
if self.profile_filename.endswith('.yaml') or\
|
|
495
|
+
self.profile_filename.endswith('.yml'):
|
|
496
|
+
yaml_content = yaml.safe_load(file)
|
|
497
|
+
file_contents = json.dumps(yaml_content)
|
|
498
|
+
profile_contents = json.loads(file_contents)
|
|
499
|
+
else:
|
|
500
|
+
profile_contents = json.load(file)
|
|
501
|
+
except json.JSONDecodeError:
|
|
502
|
+
pass
|
|
503
|
+
return profile_contents
|
|
504
|
+
|
|
505
|
+
def _setup_token_and_token_parts(self, base_url, api_key, byosnap_id) -> None:
|
|
506
|
+
'''
|
|
507
|
+
Setup the token and token parts for publishing and syncing
|
|
508
|
+
'''
|
|
509
|
+
self.token: Union[str, None] = get_composite_token(
|
|
510
|
+
base_url, api_key,
|
|
511
|
+
'byosnap', {'service_id': byosnap_id}
|
|
512
|
+
)
|
|
513
|
+
self.token_parts: Union[list, None] = ByoSnap._get_token_values(
|
|
514
|
+
self.token) if self.token is not None else None
|
|
208
515
|
|
|
209
|
-
def
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
516
|
+
def _setup_and_validate_byosnap_profile_data(self) -> None:
|
|
517
|
+
"""
|
|
518
|
+
Pre-Override Validator
|
|
519
|
+
"""
|
|
520
|
+
# Check dependencies
|
|
521
|
+
if self.path is None and self.resources_path is None:
|
|
522
|
+
snapctl_error(
|
|
523
|
+
message='Either the path or resources path is required ' +
|
|
524
|
+
'to import the BYOSnap profile.',
|
|
525
|
+
code=SNAPCTL_INPUT_ERROR
|
|
526
|
+
)
|
|
527
|
+
base_path = self.resources_path if self.resources_path else self.path
|
|
528
|
+
# Publish and Publish version
|
|
529
|
+
if not self.profile_filename:
|
|
530
|
+
self.profile_filename = ByoSnap.DEFAULT_PROFILE_NAME_JSON
|
|
213
531
|
else:
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
532
|
+
if not self.profile_filename.endswith('.json') and \
|
|
533
|
+
not self.profile_filename.endswith('.yaml') and \
|
|
534
|
+
not self.profile_filename.endswith('.yml'):
|
|
535
|
+
snapctl_error(
|
|
536
|
+
message='Invalid BYOSnap profile file. Please check the file extension' +
|
|
537
|
+
' and ensure it is either .json, .yaml, or .yml',
|
|
538
|
+
code=SNAPCTL_INPUT_ERROR
|
|
539
|
+
)
|
|
540
|
+
self.profile_path = os.path.join(
|
|
541
|
+
base_path, self.profile_filename)
|
|
542
|
+
if not os.path.isfile(self.profile_path):
|
|
220
543
|
snapctl_error(
|
|
221
544
|
"Unable to find " +
|
|
222
|
-
f"{self.
|
|
545
|
+
f"{self.profile_filename} at path {base_path}",
|
|
223
546
|
SNAPCTL_INPUT_ERROR)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
self.
|
|
547
|
+
profile_data_obj = self._get_profile_contents()
|
|
548
|
+
if not profile_data_obj:
|
|
549
|
+
snapctl_error(
|
|
550
|
+
message='Invalid BYOSnap profile JSON. Please check the JSON structure',
|
|
551
|
+
code=SNAPCTL_INPUT_ERROR
|
|
552
|
+
)
|
|
553
|
+
# IMPORTANT: This is where the profile data is set and validated
|
|
554
|
+
self.profile_data = profile_data_obj
|
|
555
|
+
ByoSnap._validate_byosnap_profile_data(self.profile_data)
|
|
556
|
+
# End: IMPORTANT: This is where the profile data is set
|
|
557
|
+
# Now apply the overrides
|
|
558
|
+
self.name = self.profile_data['name']
|
|
559
|
+
self.desc = self.profile_data['description']
|
|
560
|
+
self.platform_type = self.profile_data['platform']
|
|
561
|
+
self.language = self.profile_data['language']
|
|
562
|
+
self.prefix = self.profile_data['prefix']
|
|
232
563
|
# Setup the final ingress external port
|
|
233
564
|
final_ingress_external_port = {
|
|
234
565
|
'name': 'http',
|
|
235
566
|
'port': None
|
|
236
567
|
}
|
|
237
|
-
if 'http_port' in profile_data:
|
|
568
|
+
if 'http_port' in self.profile_data:
|
|
238
569
|
final_ingress_external_port = {
|
|
239
570
|
'name': 'http',
|
|
240
|
-
'port': profile_data['http_port']
|
|
571
|
+
'port': self.profile_data['http_port']
|
|
241
572
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
elif 'ingress' in profile_data and 'external_port' in profile_data['ingress']:
|
|
245
|
-
final_ingress_external_port = profile_data['ingress']['external_port']
|
|
573
|
+
elif 'ingress' in self.profile_data and 'external_port' in self.profile_data['ingress']:
|
|
574
|
+
final_ingress_external_port = self.profile_data['ingress']['external_port']
|
|
246
575
|
self.ingress_external_port = final_ingress_external_port
|
|
247
576
|
# Setup the final ingress internal ports
|
|
248
577
|
final_ingress_internal_ports = []
|
|
249
|
-
if 'ingress' in profile_data and 'internal_ports' in profile_data['ingress']:
|
|
250
|
-
final_ingress_internal_ports = profile_data['ingress']['internal_ports']
|
|
578
|
+
if 'ingress' in self.profile_data and 'internal_ports' in self.profile_data['ingress']:
|
|
579
|
+
final_ingress_internal_ports = self.profile_data['ingress']['internal_ports']
|
|
251
580
|
self.ingress_internal_ports = final_ingress_internal_ports
|
|
252
|
-
self.readiness_path = profile_data['readiness_probe_config']['path']
|
|
581
|
+
self.readiness_path = self.profile_data['readiness_probe_config']['path']
|
|
253
582
|
self.readiness_delay = \
|
|
254
|
-
profile_data['readiness_probe_config']['initial_delay_seconds']
|
|
255
|
-
# Validate
|
|
256
|
-
if not self.prefix or self.prefix == '':
|
|
257
|
-
snapctl_error("Missing prefix", SNAPCTL_INPUT_ERROR)
|
|
258
|
-
if not self.prefix.startswith('/'):
|
|
259
|
-
snapctl_error("Prefix should start with a forward slash (/)",
|
|
260
|
-
SNAPCTL_INPUT_ERROR)
|
|
261
|
-
if self.prefix.endswith('/'):
|
|
262
|
-
snapctl_error("Prefix should not end with a forward slash (/)",
|
|
263
|
-
SNAPCTL_INPUT_ERROR)
|
|
264
|
-
if not self.version:
|
|
265
|
-
snapctl_error("Missing version", SNAPCTL_INPUT_ERROR)
|
|
266
|
-
pattern = r'^v\d+\.\d+\.\d+$'
|
|
267
|
-
if not re.match(pattern, self.version):
|
|
268
|
-
snapctl_error("Version should be in the format vX.X.X",
|
|
269
|
-
SNAPCTL_INPUT_ERROR)
|
|
270
|
-
if not self.ingress_external_port['port']:
|
|
271
|
-
snapctl_error("Missing Ingress HTTP Port",
|
|
272
|
-
SNAPCTL_INPUT_ERROR)
|
|
273
|
-
if not isinstance(self.ingress_external_port['port'], int):
|
|
274
|
-
snapctl_error("Ingress external port should be a number",
|
|
275
|
-
SNAPCTL_INPUT_ERROR)
|
|
276
|
-
# Check internal ports
|
|
277
|
-
duplicate_name = {}
|
|
278
|
-
duplicate_port = {}
|
|
279
|
-
index = 0
|
|
280
|
-
for internal_port in self.ingress_internal_ports:
|
|
281
|
-
if 'name' not in internal_port or 'port' not in internal_port:
|
|
282
|
-
snapctl_error("Internal ports need a name and a port. Check internal port " +
|
|
283
|
-
f"number {index}.",
|
|
284
|
-
SNAPCTL_INPUT_ERROR)
|
|
285
|
-
# Confirm the name does not collide with the external port
|
|
286
|
-
if internal_port['name'] == self.ingress_external_port['name']:
|
|
287
|
-
snapctl_error("Internal port name should not be the same as " +
|
|
288
|
-
"the external port name", SNAPCTL_INPUT_ERROR)
|
|
289
|
-
# Confirm the port does not collide with the external port
|
|
290
|
-
if internal_port['port'] == self.ingress_external_port['port']:
|
|
291
|
-
snapctl_error("Internal port number should not be the same as " +
|
|
292
|
-
"the external port number", SNAPCTL_INPUT_ERROR)
|
|
293
|
-
index += 1
|
|
294
|
-
if not internal_port['port']:
|
|
295
|
-
snapctl_error("Missing internal port. Check internal port " +
|
|
296
|
-
f"number {index}",
|
|
297
|
-
SNAPCTL_INPUT_ERROR)
|
|
298
|
-
if not isinstance(internal_port['port'], int):
|
|
299
|
-
snapctl_error("Internal port should be a number. Check internal port " +
|
|
300
|
-
f"number {index}",
|
|
301
|
-
SNAPCTL_INPUT_ERROR)
|
|
302
|
-
if internal_port['name'] in duplicate_name:
|
|
303
|
-
snapctl_error("Duplicate internal port name. Check internal port " +
|
|
304
|
-
f"number {index}",
|
|
305
|
-
SNAPCTL_INPUT_ERROR)
|
|
306
|
-
if internal_port['port'] in duplicate_port:
|
|
307
|
-
snapctl_error("Duplicate internal port number. Check internal port " +
|
|
308
|
-
f"number {index}",
|
|
309
|
-
SNAPCTL_INPUT_ERROR)
|
|
310
|
-
duplicate_name[internal_port['name']] = True
|
|
311
|
-
duplicate_port[internal_port['port']] = True
|
|
312
|
-
if self.readiness_path is not None:
|
|
313
|
-
if self.readiness_path.strip() == '':
|
|
314
|
-
snapctl_error("Readiness path cannot be empty",
|
|
315
|
-
SNAPCTL_INPUT_ERROR)
|
|
316
|
-
if not self.readiness_path.strip().startswith('/'):
|
|
317
|
-
snapctl_error("Readiness path has to start with /",
|
|
318
|
-
SNAPCTL_INPUT_ERROR)
|
|
319
|
-
if self.readiness_delay is not None:
|
|
320
|
-
if self.readiness_delay < 0 or \
|
|
321
|
-
self.readiness_delay > ByoSnap.MAX_READINESS_TIMEOUT:
|
|
322
|
-
snapctl_error(
|
|
323
|
-
"Readiness delay should be between 0 " +
|
|
324
|
-
f"and {ByoSnap.MAX_READINESS_TIMEOUT}", SNAPCTL_INPUT_ERROR)
|
|
583
|
+
self.profile_data['readiness_probe_config']['initial_delay_seconds']
|
|
325
584
|
|
|
326
585
|
def _check_dependencies(self) -> None:
|
|
327
586
|
"""
|
|
@@ -372,6 +631,25 @@ class ByoSnap:
|
|
|
372
631
|
try:
|
|
373
632
|
# Login to Snapser Registry
|
|
374
633
|
if platform == 'win32':
|
|
634
|
+
# Start: Hack for Windows
|
|
635
|
+
data = {
|
|
636
|
+
"auths": {
|
|
637
|
+
"https://index.docker.io/v1/": {}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
# Path to the Docker config file, adjust the path as necessary
|
|
642
|
+
docker_config_path = os.path.expanduser(
|
|
643
|
+
'~\\.docker\\config.json')
|
|
644
|
+
|
|
645
|
+
# Ensure the directory exists
|
|
646
|
+
os.makedirs(os.path.dirname(docker_config_path), exist_ok=True)
|
|
647
|
+
|
|
648
|
+
# Write the data to the file
|
|
649
|
+
with open(docker_config_path, 'w', encoding='utf-8') as file:
|
|
650
|
+
json.dump(data, file)
|
|
651
|
+
info("Updated the docker config for docker login")
|
|
652
|
+
# End: Hack for Windows
|
|
375
653
|
response = subprocess.run([
|
|
376
654
|
'docker', 'login', '--username', ecr_repo_username,
|
|
377
655
|
'--password', ecr_repo_token, ecr_repo_url
|
|
@@ -397,7 +675,7 @@ class ByoSnap:
|
|
|
397
675
|
|
|
398
676
|
def _docker_build(self) -> None:
|
|
399
677
|
# Get the data
|
|
400
|
-
# image_tag = f'{self.
|
|
678
|
+
# image_tag = f'{self.byosnap_id}.{self.tag}'
|
|
401
679
|
build_platform = ByoSnap.DEFAULT_BUILD_PLATFORM
|
|
402
680
|
if len(self.token_parts) == 4:
|
|
403
681
|
build_platform = self.token_parts[3]
|
|
@@ -415,7 +693,7 @@ class ByoSnap:
|
|
|
415
693
|
base_path = self.resources_path
|
|
416
694
|
else:
|
|
417
695
|
base_path = self.path
|
|
418
|
-
docker_file_path = os.path.join(base_path, self.
|
|
696
|
+
docker_file_path = os.path.join(base_path, self.docker_filename)
|
|
419
697
|
|
|
420
698
|
# Warning check for architecture specific commands
|
|
421
699
|
info(f'Building on system architecture {sys_platform.machine()}')
|
|
@@ -455,7 +733,7 @@ class ByoSnap:
|
|
|
455
733
|
def _docker_tag(self) -> None:
|
|
456
734
|
# Get the data
|
|
457
735
|
ecr_repo_url = self.token_parts[0]
|
|
458
|
-
image_tag = f'{self.
|
|
736
|
+
image_tag = f'{self.byosnap_id}.{self.tag}'
|
|
459
737
|
full_ecr_repo_url = f'{ecr_repo_url}:{image_tag}'
|
|
460
738
|
progress = Progress(
|
|
461
739
|
SpinnerColumn(),
|
|
@@ -502,7 +780,7 @@ class ByoSnap:
|
|
|
502
780
|
try:
|
|
503
781
|
# Push the image
|
|
504
782
|
ecr_repo_url = self.token_parts[0]
|
|
505
|
-
image_tag = f'{self.
|
|
783
|
+
image_tag = f'{self.byosnap_id}.{self.tag}'
|
|
506
784
|
full_ecr_repo_url = f'{ecr_repo_url}:{image_tag}'
|
|
507
785
|
if platform == "win32":
|
|
508
786
|
response = subprocess.run([
|
|
@@ -557,8 +835,8 @@ class ByoSnap:
|
|
|
557
835
|
|
|
558
836
|
# Public methods
|
|
559
837
|
|
|
560
|
-
#
|
|
561
|
-
def
|
|
838
|
+
# Validate
|
|
839
|
+
def setup_and_validate_input(self) -> None:
|
|
562
840
|
"""
|
|
563
841
|
Validator
|
|
564
842
|
"""
|
|
@@ -573,21 +851,10 @@ class ByoSnap:
|
|
|
573
851
|
f"{', '.join(ByoSnap.SUBCOMMANDS)}.",
|
|
574
852
|
code=SNAPCTL_INPUT_ERROR
|
|
575
853
|
)
|
|
576
|
-
# Validate the SID
|
|
577
|
-
if not self.sid.startswith(ByoSnap.ID_PREFIX):
|
|
578
|
-
snapctl_error(
|
|
579
|
-
message="Invalid Snap ID. Valid Snap IDs start with " +
|
|
580
|
-
f"{ByoSnap.ID_PREFIX}.",
|
|
581
|
-
code=SNAPCTL_INPUT_ERROR
|
|
582
|
-
)
|
|
583
|
-
if len(self.sid) > ByoSnap.SID_CHARACTER_LIMIT:
|
|
584
|
-
snapctl_error(
|
|
585
|
-
message="Invalid Snap ID. Snap ID should be less than " +
|
|
586
|
-
f"{ByoSnap.SID_CHARACTER_LIMIT} characters",
|
|
587
|
-
code=SNAPCTL_INPUT_ERROR
|
|
588
|
-
)
|
|
589
854
|
# Validation for subcommands
|
|
590
855
|
if self.subcommand == 'create':
|
|
856
|
+
# Validator
|
|
857
|
+
ByoSnap._validate_byosnap_id(self.byosnap_id)
|
|
591
858
|
if self.name == '':
|
|
592
859
|
snapctl_error(message="Missing name", code=SNAPCTL_INPUT_ERROR)
|
|
593
860
|
if not self.language:
|
|
@@ -606,9 +873,14 @@ class ByoSnap:
|
|
|
606
873
|
code=SNAPCTL_INPUT_ERROR
|
|
607
874
|
)
|
|
608
875
|
elif self.subcommand == 'publish-image':
|
|
876
|
+
# Setup
|
|
877
|
+
self._setup_token_and_token_parts(
|
|
878
|
+
self.base_url, self.api_key, self.byosnap_id)
|
|
879
|
+
# Validator
|
|
609
880
|
if self.token_parts is None:
|
|
610
881
|
snapctl_error('Invalid token. Please reach out to your support team.',
|
|
611
882
|
SNAPCTL_INPUT_ERROR)
|
|
883
|
+
ByoSnap._validate_byosnap_id(self.byosnap_id)
|
|
612
884
|
if not self.tag:
|
|
613
885
|
snapctl_error(
|
|
614
886
|
"Missing required parameter: tag", SNAPCTL_INPUT_ERROR)
|
|
@@ -625,22 +897,43 @@ class ByoSnap:
|
|
|
625
897
|
# Check path
|
|
626
898
|
if self.resources_path:
|
|
627
899
|
docker_file_path = \
|
|
628
|
-
f"{self.resources_path}/{self.
|
|
900
|
+
f"{self.resources_path}/{self.docker_filename}"
|
|
629
901
|
else:
|
|
630
|
-
docker_file_path = f"{self.path}/{self.
|
|
902
|
+
docker_file_path = f"{self.path}/{self.docker_filename}"
|
|
631
903
|
if not self.skip_build and not os.path.isfile(docker_file_path):
|
|
632
904
|
snapctl_error(
|
|
633
905
|
"Unable to find " +
|
|
634
|
-
f"{self.
|
|
906
|
+
f"{self.docker_filename} at path {docker_file_path}",
|
|
635
907
|
SNAPCTL_INPUT_ERROR)
|
|
636
908
|
elif self.subcommand == 'upload-docs':
|
|
909
|
+
# Setup
|
|
910
|
+
self._setup_token_and_token_parts(
|
|
911
|
+
self.base_url, self.api_key, self.byosnap_id)
|
|
912
|
+
# Validator
|
|
913
|
+
ByoSnap._validate_byosnap_id(self.byosnap_id)
|
|
637
914
|
if self.token_parts is None:
|
|
638
915
|
snapctl_error('Invalid token. Please reach out to your support team.',
|
|
639
916
|
SNAPCTL_INPUT_ERROR)
|
|
640
917
|
if self.path is None and self.resources_path is None:
|
|
641
918
|
snapctl_error(
|
|
642
919
|
"Missing one of: path or resources-path parameter", SNAPCTL_INPUT_ERROR)
|
|
920
|
+
if not self.tag:
|
|
921
|
+
snapctl_error("Missing tag", SNAPCTL_INPUT_ERROR)
|
|
922
|
+
if len(self.tag.split()) > 1 or \
|
|
923
|
+
len(self.tag) > ByoSnap.TAG_CHARACTER_LIMIT:
|
|
924
|
+
snapctl_error(
|
|
925
|
+
"Tag should be a single word with maximum of " +
|
|
926
|
+
f"{ByoSnap.TAG_CHARACTER_LIMIT} characters",
|
|
927
|
+
SNAPCTL_INPUT_ERROR
|
|
928
|
+
)
|
|
643
929
|
elif self.subcommand == 'publish-version':
|
|
930
|
+
# Setup
|
|
931
|
+
self._setup_token_and_token_parts(
|
|
932
|
+
self.base_url, self.api_key, self.byosnap_id)
|
|
933
|
+
# Setup the profile data
|
|
934
|
+
self._setup_and_validate_byosnap_profile_data()
|
|
935
|
+
# Validator
|
|
936
|
+
ByoSnap._validate_byosnap_id(self.byosnap_id)
|
|
644
937
|
if self.token_parts is None:
|
|
645
938
|
snapctl_error('Invalid token. Please reach out to your support team.',
|
|
646
939
|
SNAPCTL_INPUT_ERROR)
|
|
@@ -654,7 +947,18 @@ class ByoSnap:
|
|
|
654
947
|
f"{ByoSnap.TAG_CHARACTER_LIMIT} characters",
|
|
655
948
|
SNAPCTL_INPUT_ERROR
|
|
656
949
|
)
|
|
950
|
+
if not self.version:
|
|
951
|
+
snapctl_error("Missing version", SNAPCTL_INPUT_ERROR)
|
|
952
|
+
pattern = r'^v\d+\.\d+\.\d+$'
|
|
953
|
+
if not re.match(r'^v\d+\.\d+\.\d+$', self.version):
|
|
954
|
+
snapctl_error("Version should be in the format vX.X.X",
|
|
955
|
+
SNAPCTL_INPUT_ERROR)
|
|
657
956
|
elif self.subcommand == 'update-version':
|
|
957
|
+
# Setup
|
|
958
|
+
self._setup_token_and_token_parts(
|
|
959
|
+
self.base_url, self.api_key, self.byosnap_id)
|
|
960
|
+
# Validator
|
|
961
|
+
ByoSnap._validate_byosnap_id(self.byosnap_id)
|
|
658
962
|
if self.token_parts is None:
|
|
659
963
|
snapctl_error('Invalid token. Please reach out to your support team.',
|
|
660
964
|
SNAPCTL_INPUT_ERROR)
|
|
@@ -677,6 +981,11 @@ class ByoSnap:
|
|
|
677
981
|
code=SNAPCTL_INPUT_ERROR
|
|
678
982
|
)
|
|
679
983
|
elif self.subcommand == 'sync':
|
|
984
|
+
# Setup
|
|
985
|
+
self._setup_token_and_token_parts(
|
|
986
|
+
self.base_url, self.api_key, self.byosnap_id)
|
|
987
|
+
# Validator
|
|
988
|
+
ByoSnap._validate_byosnap_id(self.byosnap_id)
|
|
680
989
|
if self.token_parts is None:
|
|
681
990
|
snapctl_error('Invalid token. Please reach out to your support team.',
|
|
682
991
|
SNAPCTL_INPUT_ERROR)
|
|
@@ -694,20 +1003,27 @@ class ByoSnap:
|
|
|
694
1003
|
# Check path
|
|
695
1004
|
if self.resources_path:
|
|
696
1005
|
docker_file_path = \
|
|
697
|
-
f"{self.resources_path}/{self.
|
|
1006
|
+
f"{self.resources_path}/{self.docker_filename}"
|
|
698
1007
|
else:
|
|
699
|
-
docker_file_path = f"{self.path}/{self.
|
|
1008
|
+
docker_file_path = f"{self.path}/{self.docker_filename}"
|
|
700
1009
|
if not self.skip_build and not os.path.isfile(docker_file_path):
|
|
701
1010
|
snapctl_error(
|
|
702
1011
|
message="Unable to find " +
|
|
703
|
-
f"{self.
|
|
1012
|
+
f"{self.docker_filename} at path {docker_file_path}",
|
|
1013
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
1014
|
+
if not self.snapend_id:
|
|
1015
|
+
snapctl_error(
|
|
1016
|
+
message="Missing required parameter: snapend-id",
|
|
704
1017
|
code=SNAPCTL_INPUT_ERROR)
|
|
705
1018
|
elif self.subcommand == 'publish':
|
|
1019
|
+
# Setup the profile data
|
|
1020
|
+
self._setup_and_validate_byosnap_profile_data()
|
|
1021
|
+
# Validator
|
|
1022
|
+
ByoSnap._validate_byosnap_id(self.byosnap_id)
|
|
706
1023
|
if not self.version:
|
|
707
1024
|
snapctl_error(message="Missing version. Version should be in the format vX.X.X",
|
|
708
1025
|
code=SNAPCTL_INPUT_ERROR)
|
|
709
|
-
|
|
710
|
-
if not re.match(pattern, self.version):
|
|
1026
|
+
if not re.match(r'^v\d+\.\d+\.\d+$', self.version):
|
|
711
1027
|
snapctl_error(message="Version should be in the format vX.X.X",
|
|
712
1028
|
code=SNAPCTL_INPUT_ERROR)
|
|
713
1029
|
if not self.skip_build and not self.path:
|
|
@@ -717,15 +1033,47 @@ class ByoSnap:
|
|
|
717
1033
|
# Check path
|
|
718
1034
|
if self.resources_path:
|
|
719
1035
|
docker_file_path = \
|
|
720
|
-
f"{self.resources_path}/{self.
|
|
1036
|
+
f"{self.resources_path}/{self.docker_filename}"
|
|
721
1037
|
else:
|
|
722
|
-
docker_file_path = f"{self.path}/{self.
|
|
1038
|
+
docker_file_path = f"{self.path}/{self.docker_filename}"
|
|
723
1039
|
if not self.skip_build and not os.path.isfile(docker_file_path):
|
|
724
1040
|
snapctl_error(
|
|
725
1041
|
message="Unable to find " +
|
|
726
|
-
f"{self.
|
|
1042
|
+
f"{self.docker_filename} at path {docker_file_path}",
|
|
727
1043
|
code=SNAPCTL_INPUT_ERROR)
|
|
728
1044
|
|
|
1045
|
+
# Run the overrides
|
|
1046
|
+
elif self.subcommand == 'generate-profile':
|
|
1047
|
+
# Setup
|
|
1048
|
+
# self._setup_token_and_token_parts(
|
|
1049
|
+
# self.base_url, self.api_key, self.byosnap_id)
|
|
1050
|
+
# Validator
|
|
1051
|
+
if not self.out_path:
|
|
1052
|
+
snapctl_error(
|
|
1053
|
+
message='Missing required parameter: out-path. ' +
|
|
1054
|
+
'Path is required for profile generation',
|
|
1055
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
1056
|
+
if not os.path.isdir(self.out_path):
|
|
1057
|
+
snapctl_error(
|
|
1058
|
+
message='Invalid out-path. ' +
|
|
1059
|
+
'Path should be a directory',
|
|
1060
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
1061
|
+
if self.profile_filename is not None:
|
|
1062
|
+
if not self.profile_filename.endswith('.json') and \
|
|
1063
|
+
not self.profile_filename.endswith('.yaml') and \
|
|
1064
|
+
not self.profile_filename.endswith('.yml'):
|
|
1065
|
+
snapctl_error(
|
|
1066
|
+
message='Invalid BYOSnap profile file. Please check the file extension' +
|
|
1067
|
+
' and ensure it is either .json, .yaml, or .yml',
|
|
1068
|
+
code=SNAPCTL_INPUT_ERROR
|
|
1069
|
+
)
|
|
1070
|
+
elif self.subcommand == 'validate-profile':
|
|
1071
|
+
# # Setup
|
|
1072
|
+
# self._setup_token_and_token_parts(
|
|
1073
|
+
# self.base_url, self.api_key, self.byosnap_id)
|
|
1074
|
+
# Setup the profile data
|
|
1075
|
+
self._setup_and_validate_byosnap_profile_data()
|
|
1076
|
+
|
|
729
1077
|
# Basic methods
|
|
730
1078
|
|
|
731
1079
|
def build(self) -> None:
|
|
@@ -780,7 +1128,7 @@ class ByoSnap:
|
|
|
780
1128
|
with open(swagger_file, "rb") as attachment_file:
|
|
781
1129
|
url = (
|
|
782
1130
|
f"{self.base_url}/v1/snapser-api/byosnaps/"
|
|
783
|
-
f"{self.
|
|
1131
|
+
f"{self.byosnap_id}/docs/{self.tag}/openapispec"
|
|
784
1132
|
)
|
|
785
1133
|
test_res = requests.post(
|
|
786
1134
|
url, files={"attachment": attachment_file},
|
|
@@ -809,7 +1157,7 @@ class ByoSnap:
|
|
|
809
1157
|
with open(readme_file, "rb") as attachment_file:
|
|
810
1158
|
url = (
|
|
811
1159
|
f"{self.base_url}/v1/snapser-api/byosnaps/"
|
|
812
|
-
f"{self.
|
|
1160
|
+
f"{self.byosnap_id}/docs/{self.tag}/markdown"
|
|
813
1161
|
)
|
|
814
1162
|
test_res = requests.post(
|
|
815
1163
|
url, files={"attachment": attachment_file},
|
|
@@ -841,7 +1189,7 @@ class ByoSnap:
|
|
|
841
1189
|
with open(file_path, "rb") as attachment_file:
|
|
842
1190
|
url = (
|
|
843
1191
|
f"{self.base_url}/v1/snapser-api/byosnaps/"
|
|
844
|
-
f"{self.
|
|
1192
|
+
f"{self.byosnap_id}/docs/{self.tag}/tools"
|
|
845
1193
|
)
|
|
846
1194
|
test_res = requests.post(
|
|
847
1195
|
url, files={"attachment": attachment_file},
|
|
@@ -879,7 +1227,7 @@ class ByoSnap:
|
|
|
879
1227
|
progress.add_task(description='Creating your snap...', total=None)
|
|
880
1228
|
try:
|
|
881
1229
|
payload = {
|
|
882
|
-
"service_id": self.
|
|
1230
|
+
"service_id": self.byosnap_id,
|
|
883
1231
|
"name": self.name,
|
|
884
1232
|
"description": self.desc,
|
|
885
1233
|
"platform": self.platform_type,
|
|
@@ -965,16 +1313,20 @@ class ByoSnap:
|
|
|
965
1313
|
progress.add_task(
|
|
966
1314
|
description='Publishing your snap...', total=None)
|
|
967
1315
|
try:
|
|
968
|
-
profile_data =
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1316
|
+
profile_data = self._get_profile_contents()
|
|
1317
|
+
dev_template = None
|
|
1318
|
+
if 'dev_template' in profile_data:
|
|
1319
|
+
dev_template = profile_data['dev_template']
|
|
1320
|
+
stage_template = None
|
|
1321
|
+
if 'stage_template' in profile_data:
|
|
1322
|
+
stage_template = profile_data['stage_template']
|
|
1323
|
+
prod_template = None
|
|
1324
|
+
if 'prod_template' in profile_data:
|
|
1325
|
+
prod_template = profile_data['prod_template']
|
|
974
1326
|
payload = {
|
|
975
1327
|
"version": self.version,
|
|
976
1328
|
"image_tag": self.tag,
|
|
977
|
-
"base_url": f"{self.prefix}/{self.
|
|
1329
|
+
"base_url": f"{self.prefix}/{self.byosnap_id}",
|
|
978
1330
|
"ingress": {
|
|
979
1331
|
"external_port": self.ingress_external_port,
|
|
980
1332
|
"internal_ports": self.ingress_internal_ports
|
|
@@ -983,14 +1335,14 @@ class ByoSnap:
|
|
|
983
1335
|
"path": self.readiness_path,
|
|
984
1336
|
"initial_delay_seconds": self.readiness_delay
|
|
985
1337
|
},
|
|
986
|
-
"dev_template":
|
|
987
|
-
"stage_template":
|
|
988
|
-
"prod_template":
|
|
1338
|
+
"dev_template": dev_template,
|
|
1339
|
+
"stage_template": stage_template,
|
|
1340
|
+
"prod_template": prod_template,
|
|
989
1341
|
# Currently not supported so we are just hardcoding an empty list
|
|
990
1342
|
"egress": {"ports": []},
|
|
991
1343
|
}
|
|
992
1344
|
res = requests.post(
|
|
993
|
-
f"{self.base_url}/v1/snapser-api/byosnaps/{self.
|
|
1345
|
+
f"{self.base_url}/v1/snapser-api/byosnaps/{self.byosnap_id}/versions",
|
|
994
1346
|
json=payload, headers={'api-key': self.api_key},
|
|
995
1347
|
timeout=SERVER_CALL_TIMEOUT
|
|
996
1348
|
)
|
|
@@ -1047,7 +1399,7 @@ class ByoSnap:
|
|
|
1047
1399
|
'image_tag': self.tag,
|
|
1048
1400
|
}
|
|
1049
1401
|
res = requests.patch(
|
|
1050
|
-
f"{self.base_url}/v1/snapser-api/byosnaps/{self.
|
|
1402
|
+
f"{self.base_url}/v1/snapser-api/byosnaps/{self.byosnap_id}/versions/{self.version}",
|
|
1051
1403
|
json=payload, headers={'api-key': self.api_key},
|
|
1052
1404
|
timeout=SERVER_CALL_TIMEOUT
|
|
1053
1405
|
)
|
|
@@ -1095,7 +1447,7 @@ class ByoSnap:
|
|
|
1095
1447
|
try:
|
|
1096
1448
|
# Attempt to create a BYOSnap but no worries if it fails
|
|
1097
1449
|
payload = {
|
|
1098
|
-
"service_id": self.
|
|
1450
|
+
"service_id": self.byosnap_id,
|
|
1099
1451
|
"name": self.name,
|
|
1100
1452
|
"description": self.desc,
|
|
1101
1453
|
"platform": self.platform_type,
|
|
@@ -1120,7 +1472,7 @@ class ByoSnap:
|
|
|
1120
1472
|
self.tag = self.version
|
|
1121
1473
|
# Setup the token and token parts
|
|
1122
1474
|
self._setup_token_and_token_parts(
|
|
1123
|
-
self.base_url, self.api_key, self.
|
|
1475
|
+
self.base_url, self.api_key, self.byosnap_id)
|
|
1124
1476
|
# Now publish the image
|
|
1125
1477
|
self.publish_image(no_exit=True)
|
|
1126
1478
|
# Now publish the version
|
|
@@ -1140,7 +1492,7 @@ class ByoSnap:
|
|
|
1140
1492
|
self.tag = f'{self.version}-{int(time.time())}'
|
|
1141
1493
|
self.publish_image(no_exit=True)
|
|
1142
1494
|
self.update_version(no_exit=True)
|
|
1143
|
-
byosnap_list: str = f"{self.
|
|
1495
|
+
byosnap_list: str = f"{self.byosnap_id}:{self.version}"
|
|
1144
1496
|
snapend = Snapend(
|
|
1145
1497
|
subcommand='update', base_url=self.base_url, api_key=self.api_key,
|
|
1146
1498
|
snapend_id=self.snapend_id, byosnaps=byosnap_list, blocking=self.blocking
|
|
@@ -1152,3 +1504,52 @@ class ByoSnap:
|
|
|
1152
1504
|
message='Exception: Unable to update a ' +
|
|
1153
1505
|
f' version for your snap. Exception: {e}',
|
|
1154
1506
|
code=SNAPCTL_BYOSNAP_UPDATE_VERSION_ERROR)
|
|
1507
|
+
|
|
1508
|
+
def generate_profile(self, no_exit: bool = False) -> None:
|
|
1509
|
+
"""
|
|
1510
|
+
Generate snapser-byosnap-profile.json
|
|
1511
|
+
"""
|
|
1512
|
+
progress = Progress(
|
|
1513
|
+
SpinnerColumn(),
|
|
1514
|
+
TextColumn("[progress.description]{task.description}"),
|
|
1515
|
+
transient=True,
|
|
1516
|
+
)
|
|
1517
|
+
progress.start()
|
|
1518
|
+
progress.add_task(
|
|
1519
|
+
description='Generating BYOSnap profile...', total=None)
|
|
1520
|
+
try:
|
|
1521
|
+
if self.out_path is not None:
|
|
1522
|
+
file_save_path = os.path.join(
|
|
1523
|
+
self.out_path, self.profile_filename)
|
|
1524
|
+
else:
|
|
1525
|
+
file_save_path = os.path.join(
|
|
1526
|
+
os.getcwd(), self.profile_filename)
|
|
1527
|
+
file_written = ByoSnap._handle_output_file(
|
|
1528
|
+
f"{ByoSnap.PROFILE_FILES_PATH}{
|
|
1529
|
+
self.profile_filename}", file_save_path
|
|
1530
|
+
)
|
|
1531
|
+
if file_written:
|
|
1532
|
+
snapctl_success(
|
|
1533
|
+
message="BYOSNAP Profile generation successful. " +
|
|
1534
|
+
f"{self.profile_filename} saved at {
|
|
1535
|
+
file_save_path}",
|
|
1536
|
+
progress=progress,
|
|
1537
|
+
no_exit=no_exit
|
|
1538
|
+
)
|
|
1539
|
+
return
|
|
1540
|
+
except (IOError, OSError) as file_error:
|
|
1541
|
+
snapctl_error(
|
|
1542
|
+
message=f"File error: {file_error}",
|
|
1543
|
+
code=SNAPCTL_BYOSNAP_GENERATE_PROFILE_ERROR, progress=progress)
|
|
1544
|
+
snapctl_error(
|
|
1545
|
+
message="Failed to generate BYOSNAP Profile",
|
|
1546
|
+
code=SNAPCTL_BYOSNAP_GENERATE_PROFILE_ERROR,
|
|
1547
|
+
progress=progress
|
|
1548
|
+
)
|
|
1549
|
+
|
|
1550
|
+
def validate_profile(self) -> None:
|
|
1551
|
+
'''
|
|
1552
|
+
Validate the profile
|
|
1553
|
+
'''
|
|
1554
|
+
# Note all the validation is already happening in the constructor
|
|
1555
|
+
return snapctl_success(message='BYOSNAP profile validated.')
|