snapctl 0.44.1__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 +20 -20
- snapctl/commands/byosnap.py +595 -226
- 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 +80 -66
- {snapctl-0.44.1.dist-info → snapctl-0.46.0.dist-info}/METADATA +199 -98
- snapctl-0.46.0.dist-info/RECORD +27 -0
- snapctl-0.44.1.dist-info/RECORD +0 -23
- {snapctl-0.44.1.dist-info → snapctl-0.46.0.dist-info}/LICENSE +0 -0
- {snapctl-0.44.1.dist-info → snapctl-0.46.0.dist-info}/WHEEL +0 -0
- {snapctl-0.44.1.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',
|
|
167
|
+
code=SNAPCTL_INPUT_ERROR
|
|
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',
|
|
150
320
|
code=SNAPCTL_INPUT_ERROR
|
|
151
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,175 +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
|
|
208
485
|
|
|
209
|
-
def
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
|
515
|
+
|
|
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
|
-
# check that prefix does not contain multiple slashes
|
|
265
|
-
if self.prefix.count('/') > 1:
|
|
266
|
-
snapctl_error("Prefix should not contain multiple path segments",
|
|
267
|
-
SNAPCTL_INPUT_ERROR)
|
|
268
|
-
if not self.version:
|
|
269
|
-
snapctl_error("Missing version", SNAPCTL_INPUT_ERROR)
|
|
270
|
-
pattern = r'^v\d+\.\d+\.\d+$'
|
|
271
|
-
if not re.match(pattern, self.version):
|
|
272
|
-
snapctl_error("Version should be in the format vX.X.X",
|
|
273
|
-
SNAPCTL_INPUT_ERROR)
|
|
274
|
-
if not self.ingress_external_port['port']:
|
|
275
|
-
snapctl_error("Missing Ingress HTTP Port",
|
|
276
|
-
SNAPCTL_INPUT_ERROR)
|
|
277
|
-
if not isinstance(self.ingress_external_port['port'], int):
|
|
278
|
-
snapctl_error("Ingress external port should be a number",
|
|
279
|
-
SNAPCTL_INPUT_ERROR)
|
|
280
|
-
# Check internal ports
|
|
281
|
-
duplicate_name = {}
|
|
282
|
-
duplicate_port = {}
|
|
283
|
-
index = 0
|
|
284
|
-
for internal_port in self.ingress_internal_ports:
|
|
285
|
-
if 'name' not in internal_port or 'port' not in internal_port:
|
|
286
|
-
snapctl_error("Internal ports need a name and a port. Check internal port " +
|
|
287
|
-
f"number {index}.",
|
|
288
|
-
SNAPCTL_INPUT_ERROR)
|
|
289
|
-
# Confirm the name does not collide with the external port
|
|
290
|
-
if internal_port['name'] == self.ingress_external_port['name']:
|
|
291
|
-
snapctl_error("Internal port name should not be the same as " +
|
|
292
|
-
"the external port name", SNAPCTL_INPUT_ERROR)
|
|
293
|
-
# Confirm the port does not collide with the external port
|
|
294
|
-
if internal_port['port'] == self.ingress_external_port['port']:
|
|
295
|
-
snapctl_error("Internal port number should not be the same as " +
|
|
296
|
-
"the external port number", SNAPCTL_INPUT_ERROR)
|
|
297
|
-
index += 1
|
|
298
|
-
if not internal_port['port']:
|
|
299
|
-
snapctl_error("Missing internal port. Check internal port " +
|
|
300
|
-
f"number {index}",
|
|
301
|
-
SNAPCTL_INPUT_ERROR)
|
|
302
|
-
if not isinstance(internal_port['port'], int):
|
|
303
|
-
snapctl_error("Internal port should be a number. Check internal port " +
|
|
304
|
-
f"number {index}",
|
|
305
|
-
SNAPCTL_INPUT_ERROR)
|
|
306
|
-
if internal_port['name'] in duplicate_name:
|
|
307
|
-
snapctl_error("Duplicate internal port name. Check internal port " +
|
|
308
|
-
f"number {index}",
|
|
309
|
-
SNAPCTL_INPUT_ERROR)
|
|
310
|
-
if internal_port['port'] in duplicate_port:
|
|
311
|
-
snapctl_error("Duplicate internal port number. Check internal port " +
|
|
312
|
-
f"number {index}",
|
|
313
|
-
SNAPCTL_INPUT_ERROR)
|
|
314
|
-
duplicate_name[internal_port['name']] = True
|
|
315
|
-
duplicate_port[internal_port['port']] = True
|
|
316
|
-
if self.readiness_path is not None:
|
|
317
|
-
if self.readiness_path.strip() == '':
|
|
318
|
-
snapctl_error("Readiness path cannot be empty",
|
|
319
|
-
SNAPCTL_INPUT_ERROR)
|
|
320
|
-
if not self.readiness_path.strip().startswith('/'):
|
|
321
|
-
snapctl_error("Readiness path has to start with /",
|
|
322
|
-
SNAPCTL_INPUT_ERROR)
|
|
323
|
-
if self.readiness_delay is not None:
|
|
324
|
-
if self.readiness_delay < 0 or \
|
|
325
|
-
self.readiness_delay > ByoSnap.MAX_READINESS_TIMEOUT:
|
|
326
|
-
snapctl_error(
|
|
327
|
-
"Readiness delay should be between 0 " +
|
|
328
|
-
f"and {ByoSnap.MAX_READINESS_TIMEOUT}", SNAPCTL_INPUT_ERROR)
|
|
583
|
+
self.profile_data['readiness_probe_config']['initial_delay_seconds']
|
|
329
584
|
|
|
330
585
|
def _check_dependencies(self) -> None:
|
|
331
586
|
"""
|
|
@@ -420,7 +675,7 @@ class ByoSnap:
|
|
|
420
675
|
|
|
421
676
|
def _docker_build(self) -> None:
|
|
422
677
|
# Get the data
|
|
423
|
-
# image_tag = f'{self.
|
|
678
|
+
# image_tag = f'{self.byosnap_id}.{self.tag}'
|
|
424
679
|
build_platform = ByoSnap.DEFAULT_BUILD_PLATFORM
|
|
425
680
|
if len(self.token_parts) == 4:
|
|
426
681
|
build_platform = self.token_parts[3]
|
|
@@ -438,7 +693,7 @@ class ByoSnap:
|
|
|
438
693
|
base_path = self.resources_path
|
|
439
694
|
else:
|
|
440
695
|
base_path = self.path
|
|
441
|
-
docker_file_path = os.path.join(base_path, self.
|
|
696
|
+
docker_file_path = os.path.join(base_path, self.docker_filename)
|
|
442
697
|
|
|
443
698
|
# Warning check for architecture specific commands
|
|
444
699
|
info(f'Building on system architecture {sys_platform.machine()}')
|
|
@@ -478,7 +733,7 @@ class ByoSnap:
|
|
|
478
733
|
def _docker_tag(self) -> None:
|
|
479
734
|
# Get the data
|
|
480
735
|
ecr_repo_url = self.token_parts[0]
|
|
481
|
-
image_tag = f'{self.
|
|
736
|
+
image_tag = f'{self.byosnap_id}.{self.tag}'
|
|
482
737
|
full_ecr_repo_url = f'{ecr_repo_url}:{image_tag}'
|
|
483
738
|
progress = Progress(
|
|
484
739
|
SpinnerColumn(),
|
|
@@ -525,7 +780,7 @@ class ByoSnap:
|
|
|
525
780
|
try:
|
|
526
781
|
# Push the image
|
|
527
782
|
ecr_repo_url = self.token_parts[0]
|
|
528
|
-
image_tag = f'{self.
|
|
783
|
+
image_tag = f'{self.byosnap_id}.{self.tag}'
|
|
529
784
|
full_ecr_repo_url = f'{ecr_repo_url}:{image_tag}'
|
|
530
785
|
if platform == "win32":
|
|
531
786
|
response = subprocess.run([
|
|
@@ -580,8 +835,8 @@ class ByoSnap:
|
|
|
580
835
|
|
|
581
836
|
# Public methods
|
|
582
837
|
|
|
583
|
-
#
|
|
584
|
-
def
|
|
838
|
+
# Validate
|
|
839
|
+
def setup_and_validate_input(self) -> None:
|
|
585
840
|
"""
|
|
586
841
|
Validator
|
|
587
842
|
"""
|
|
@@ -596,21 +851,10 @@ class ByoSnap:
|
|
|
596
851
|
f"{', '.join(ByoSnap.SUBCOMMANDS)}.",
|
|
597
852
|
code=SNAPCTL_INPUT_ERROR
|
|
598
853
|
)
|
|
599
|
-
# Validate the SID
|
|
600
|
-
if not self.sid.startswith(ByoSnap.ID_PREFIX):
|
|
601
|
-
snapctl_error(
|
|
602
|
-
message="Invalid Snap ID. Valid Snap IDs start with " +
|
|
603
|
-
f"{ByoSnap.ID_PREFIX}.",
|
|
604
|
-
code=SNAPCTL_INPUT_ERROR
|
|
605
|
-
)
|
|
606
|
-
if len(self.sid) > ByoSnap.SID_CHARACTER_LIMIT:
|
|
607
|
-
snapctl_error(
|
|
608
|
-
message="Invalid Snap ID. Snap ID should be less than " +
|
|
609
|
-
f"{ByoSnap.SID_CHARACTER_LIMIT} characters",
|
|
610
|
-
code=SNAPCTL_INPUT_ERROR
|
|
611
|
-
)
|
|
612
854
|
# Validation for subcommands
|
|
613
855
|
if self.subcommand == 'create':
|
|
856
|
+
# Validator
|
|
857
|
+
ByoSnap._validate_byosnap_id(self.byosnap_id)
|
|
614
858
|
if self.name == '':
|
|
615
859
|
snapctl_error(message="Missing name", code=SNAPCTL_INPUT_ERROR)
|
|
616
860
|
if not self.language:
|
|
@@ -629,9 +873,14 @@ class ByoSnap:
|
|
|
629
873
|
code=SNAPCTL_INPUT_ERROR
|
|
630
874
|
)
|
|
631
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
|
|
632
880
|
if self.token_parts is None:
|
|
633
881
|
snapctl_error('Invalid token. Please reach out to your support team.',
|
|
634
882
|
SNAPCTL_INPUT_ERROR)
|
|
883
|
+
ByoSnap._validate_byosnap_id(self.byosnap_id)
|
|
635
884
|
if not self.tag:
|
|
636
885
|
snapctl_error(
|
|
637
886
|
"Missing required parameter: tag", SNAPCTL_INPUT_ERROR)
|
|
@@ -648,15 +897,20 @@ class ByoSnap:
|
|
|
648
897
|
# Check path
|
|
649
898
|
if self.resources_path:
|
|
650
899
|
docker_file_path = \
|
|
651
|
-
f"{self.resources_path}/{self.
|
|
900
|
+
f"{self.resources_path}/{self.docker_filename}"
|
|
652
901
|
else:
|
|
653
|
-
docker_file_path = f"{self.path}/{self.
|
|
902
|
+
docker_file_path = f"{self.path}/{self.docker_filename}"
|
|
654
903
|
if not self.skip_build and not os.path.isfile(docker_file_path):
|
|
655
904
|
snapctl_error(
|
|
656
905
|
"Unable to find " +
|
|
657
|
-
f"{self.
|
|
906
|
+
f"{self.docker_filename} at path {docker_file_path}",
|
|
658
907
|
SNAPCTL_INPUT_ERROR)
|
|
659
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)
|
|
660
914
|
if self.token_parts is None:
|
|
661
915
|
snapctl_error('Invalid token. Please reach out to your support team.',
|
|
662
916
|
SNAPCTL_INPUT_ERROR)
|
|
@@ -673,6 +927,13 @@ class ByoSnap:
|
|
|
673
927
|
SNAPCTL_INPUT_ERROR
|
|
674
928
|
)
|
|
675
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)
|
|
676
937
|
if self.token_parts is None:
|
|
677
938
|
snapctl_error('Invalid token. Please reach out to your support team.',
|
|
678
939
|
SNAPCTL_INPUT_ERROR)
|
|
@@ -686,7 +947,18 @@ class ByoSnap:
|
|
|
686
947
|
f"{ByoSnap.TAG_CHARACTER_LIMIT} characters",
|
|
687
948
|
SNAPCTL_INPUT_ERROR
|
|
688
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)
|
|
689
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)
|
|
690
962
|
if self.token_parts is None:
|
|
691
963
|
snapctl_error('Invalid token. Please reach out to your support team.',
|
|
692
964
|
SNAPCTL_INPUT_ERROR)
|
|
@@ -709,6 +981,11 @@ class ByoSnap:
|
|
|
709
981
|
code=SNAPCTL_INPUT_ERROR
|
|
710
982
|
)
|
|
711
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)
|
|
712
989
|
if self.token_parts is None:
|
|
713
990
|
snapctl_error('Invalid token. Please reach out to your support team.',
|
|
714
991
|
SNAPCTL_INPUT_ERROR)
|
|
@@ -726,20 +1003,27 @@ class ByoSnap:
|
|
|
726
1003
|
# Check path
|
|
727
1004
|
if self.resources_path:
|
|
728
1005
|
docker_file_path = \
|
|
729
|
-
f"{self.resources_path}/{self.
|
|
1006
|
+
f"{self.resources_path}/{self.docker_filename}"
|
|
730
1007
|
else:
|
|
731
|
-
docker_file_path = f"{self.path}/{self.
|
|
1008
|
+
docker_file_path = f"{self.path}/{self.docker_filename}"
|
|
732
1009
|
if not self.skip_build and not os.path.isfile(docker_file_path):
|
|
733
1010
|
snapctl_error(
|
|
734
1011
|
message="Unable to find " +
|
|
735
|
-
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",
|
|
736
1017
|
code=SNAPCTL_INPUT_ERROR)
|
|
737
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)
|
|
738
1023
|
if not self.version:
|
|
739
1024
|
snapctl_error(message="Missing version. Version should be in the format vX.X.X",
|
|
740
1025
|
code=SNAPCTL_INPUT_ERROR)
|
|
741
|
-
|
|
742
|
-
if not re.match(pattern, self.version):
|
|
1026
|
+
if not re.match(r'^v\d+\.\d+\.\d+$', self.version):
|
|
743
1027
|
snapctl_error(message="Version should be in the format vX.X.X",
|
|
744
1028
|
code=SNAPCTL_INPUT_ERROR)
|
|
745
1029
|
if not self.skip_build and not self.path:
|
|
@@ -749,14 +1033,46 @@ class ByoSnap:
|
|
|
749
1033
|
# Check path
|
|
750
1034
|
if self.resources_path:
|
|
751
1035
|
docker_file_path = \
|
|
752
|
-
f"{self.resources_path}/{self.
|
|
1036
|
+
f"{self.resources_path}/{self.docker_filename}"
|
|
753
1037
|
else:
|
|
754
|
-
docker_file_path = f"{self.path}/{self.
|
|
1038
|
+
docker_file_path = f"{self.path}/{self.docker_filename}"
|
|
755
1039
|
if not self.skip_build and not os.path.isfile(docker_file_path):
|
|
756
1040
|
snapctl_error(
|
|
757
1041
|
message="Unable to find " +
|
|
758
|
-
f"{self.
|
|
1042
|
+
f"{self.docker_filename} at path {docker_file_path}",
|
|
1043
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
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',
|
|
759
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()
|
|
760
1076
|
|
|
761
1077
|
# Basic methods
|
|
762
1078
|
|
|
@@ -812,7 +1128,7 @@ class ByoSnap:
|
|
|
812
1128
|
with open(swagger_file, "rb") as attachment_file:
|
|
813
1129
|
url = (
|
|
814
1130
|
f"{self.base_url}/v1/snapser-api/byosnaps/"
|
|
815
|
-
f"{self.
|
|
1131
|
+
f"{self.byosnap_id}/docs/{self.tag}/openapispec"
|
|
816
1132
|
)
|
|
817
1133
|
test_res = requests.post(
|
|
818
1134
|
url, files={"attachment": attachment_file},
|
|
@@ -841,7 +1157,7 @@ class ByoSnap:
|
|
|
841
1157
|
with open(readme_file, "rb") as attachment_file:
|
|
842
1158
|
url = (
|
|
843
1159
|
f"{self.base_url}/v1/snapser-api/byosnaps/"
|
|
844
|
-
f"{self.
|
|
1160
|
+
f"{self.byosnap_id}/docs/{self.tag}/markdown"
|
|
845
1161
|
)
|
|
846
1162
|
test_res = requests.post(
|
|
847
1163
|
url, files={"attachment": attachment_file},
|
|
@@ -873,7 +1189,7 @@ class ByoSnap:
|
|
|
873
1189
|
with open(file_path, "rb") as attachment_file:
|
|
874
1190
|
url = (
|
|
875
1191
|
f"{self.base_url}/v1/snapser-api/byosnaps/"
|
|
876
|
-
f"{self.
|
|
1192
|
+
f"{self.byosnap_id}/docs/{self.tag}/tools"
|
|
877
1193
|
)
|
|
878
1194
|
test_res = requests.post(
|
|
879
1195
|
url, files={"attachment": attachment_file},
|
|
@@ -911,7 +1227,7 @@ class ByoSnap:
|
|
|
911
1227
|
progress.add_task(description='Creating your snap...', total=None)
|
|
912
1228
|
try:
|
|
913
1229
|
payload = {
|
|
914
|
-
"service_id": self.
|
|
1230
|
+
"service_id": self.byosnap_id,
|
|
915
1231
|
"name": self.name,
|
|
916
1232
|
"description": self.desc,
|
|
917
1233
|
"platform": self.platform_type,
|
|
@@ -997,16 +1313,20 @@ class ByoSnap:
|
|
|
997
1313
|
progress.add_task(
|
|
998
1314
|
description='Publishing your snap...', total=None)
|
|
999
1315
|
try:
|
|
1000
|
-
profile_data =
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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']
|
|
1006
1326
|
payload = {
|
|
1007
1327
|
"version": self.version,
|
|
1008
1328
|
"image_tag": self.tag,
|
|
1009
|
-
"base_url": f"{self.prefix}/{self.
|
|
1329
|
+
"base_url": f"{self.prefix}/{self.byosnap_id}",
|
|
1010
1330
|
"ingress": {
|
|
1011
1331
|
"external_port": self.ingress_external_port,
|
|
1012
1332
|
"internal_ports": self.ingress_internal_ports
|
|
@@ -1015,14 +1335,14 @@ class ByoSnap:
|
|
|
1015
1335
|
"path": self.readiness_path,
|
|
1016
1336
|
"initial_delay_seconds": self.readiness_delay
|
|
1017
1337
|
},
|
|
1018
|
-
"dev_template":
|
|
1019
|
-
"stage_template":
|
|
1020
|
-
"prod_template":
|
|
1338
|
+
"dev_template": dev_template,
|
|
1339
|
+
"stage_template": stage_template,
|
|
1340
|
+
"prod_template": prod_template,
|
|
1021
1341
|
# Currently not supported so we are just hardcoding an empty list
|
|
1022
1342
|
"egress": {"ports": []},
|
|
1023
1343
|
}
|
|
1024
1344
|
res = requests.post(
|
|
1025
|
-
f"{self.base_url}/v1/snapser-api/byosnaps/{self.
|
|
1345
|
+
f"{self.base_url}/v1/snapser-api/byosnaps/{self.byosnap_id}/versions",
|
|
1026
1346
|
json=payload, headers={'api-key': self.api_key},
|
|
1027
1347
|
timeout=SERVER_CALL_TIMEOUT
|
|
1028
1348
|
)
|
|
@@ -1079,7 +1399,7 @@ class ByoSnap:
|
|
|
1079
1399
|
'image_tag': self.tag,
|
|
1080
1400
|
}
|
|
1081
1401
|
res = requests.patch(
|
|
1082
|
-
f"{self.base_url}/v1/snapser-api/byosnaps/{self.
|
|
1402
|
+
f"{self.base_url}/v1/snapser-api/byosnaps/{self.byosnap_id}/versions/{self.version}",
|
|
1083
1403
|
json=payload, headers={'api-key': self.api_key},
|
|
1084
1404
|
timeout=SERVER_CALL_TIMEOUT
|
|
1085
1405
|
)
|
|
@@ -1127,7 +1447,7 @@ class ByoSnap:
|
|
|
1127
1447
|
try:
|
|
1128
1448
|
# Attempt to create a BYOSnap but no worries if it fails
|
|
1129
1449
|
payload = {
|
|
1130
|
-
"service_id": self.
|
|
1450
|
+
"service_id": self.byosnap_id,
|
|
1131
1451
|
"name": self.name,
|
|
1132
1452
|
"description": self.desc,
|
|
1133
1453
|
"platform": self.platform_type,
|
|
@@ -1152,7 +1472,7 @@ class ByoSnap:
|
|
|
1152
1472
|
self.tag = self.version
|
|
1153
1473
|
# Setup the token and token parts
|
|
1154
1474
|
self._setup_token_and_token_parts(
|
|
1155
|
-
self.base_url, self.api_key, self.
|
|
1475
|
+
self.base_url, self.api_key, self.byosnap_id)
|
|
1156
1476
|
# Now publish the image
|
|
1157
1477
|
self.publish_image(no_exit=True)
|
|
1158
1478
|
# Now publish the version
|
|
@@ -1172,7 +1492,7 @@ class ByoSnap:
|
|
|
1172
1492
|
self.tag = f'{self.version}-{int(time.time())}'
|
|
1173
1493
|
self.publish_image(no_exit=True)
|
|
1174
1494
|
self.update_version(no_exit=True)
|
|
1175
|
-
byosnap_list: str = f"{self.
|
|
1495
|
+
byosnap_list: str = f"{self.byosnap_id}:{self.version}"
|
|
1176
1496
|
snapend = Snapend(
|
|
1177
1497
|
subcommand='update', base_url=self.base_url, api_key=self.api_key,
|
|
1178
1498
|
snapend_id=self.snapend_id, byosnaps=byosnap_list, blocking=self.blocking
|
|
@@ -1184,3 +1504,52 @@ class ByoSnap:
|
|
|
1184
1504
|
message='Exception: Unable to update a ' +
|
|
1185
1505
|
f' version for your snap. Exception: {e}',
|
|
1186
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.')
|