snapctl 0.44.1__py3-none-any.whl → 0.46.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of snapctl might be problematic. Click here for more details.
- snapctl/commands/byogs.py +20 -20
- snapctl/commands/byosnap.py +593 -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.1.dist-info}/METADATA +199 -98
- snapctl-0.46.1.dist-info/RECORD +27 -0
- snapctl-0.44.1.dist-info/RECORD +0 -23
- {snapctl-0.44.1.dist-info → snapctl-0.46.1.dist-info}/LICENSE +0 -0
- {snapctl-0.44.1.dist-info → snapctl-0.46.1.dist-info}/WHEEL +0 -0
- {snapctl-0.44.1.dist-info → snapctl-0.46.1.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,222 @@ 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 {profile}.env_params #{env_index}.",
|
|
449
|
+
code=SNAPCTL_INPUT_ERROR
|
|
450
|
+
)
|
|
451
|
+
if env_param['value'] is None or env_param['value'].strip() == '':
|
|
452
|
+
snapctl_error(
|
|
453
|
+
message='Invalid env_params value in BYOSnap profile. env_params value ' +
|
|
454
|
+
'should not be empty. ' +
|
|
455
|
+
f"Check the value entry at {profile}.env_params #{env_index}.",
|
|
456
|
+
code=SNAPCTL_INPUT_ERROR
|
|
457
|
+
)
|
|
458
|
+
return profile_data
|
|
459
|
+
|
|
460
|
+
@staticmethod
|
|
461
|
+
def _validate_byosnap_id(byosnap_id: str) -> None:
|
|
462
|
+
if not byosnap_id.startswith(ByoSnap.ID_PREFIX):
|
|
199
463
|
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',
|
|
464
|
+
message="Invalid Snap ID. Valid Snap IDs start with " +
|
|
465
|
+
f"{ByoSnap.ID_PREFIX}.",
|
|
205
466
|
code=SNAPCTL_INPUT_ERROR
|
|
206
467
|
)
|
|
207
|
-
|
|
468
|
+
if len(byosnap_id) > ByoSnap.SID_CHARACTER_LIMIT:
|
|
469
|
+
snapctl_error(
|
|
470
|
+
message="Invalid Snap ID. Snap ID should be less than " +
|
|
471
|
+
f"{ByoSnap.SID_CHARACTER_LIMIT} characters",
|
|
472
|
+
code=SNAPCTL_INPUT_ERROR
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
@staticmethod
|
|
476
|
+
def _handle_output_file(input_filepath, output_filepath) -> bool:
|
|
477
|
+
file_written = False
|
|
478
|
+
with open(input_filepath, 'r') as in_file, open(output_filepath, 'w') as outfile:
|
|
479
|
+
for line in in_file:
|
|
480
|
+
outfile.write(line)
|
|
481
|
+
file_written = True
|
|
482
|
+
return file_written
|
|
208
483
|
|
|
209
|
-
def
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
484
|
+
def _get_profile_contents(self) -> dict:
|
|
485
|
+
"""
|
|
486
|
+
Get the BYOSNap profile contents
|
|
487
|
+
based on if the user has a YAML or JSON file
|
|
488
|
+
"""
|
|
489
|
+
profile_contents = {}
|
|
490
|
+
with open(self.profile_path, 'rb') as file:
|
|
491
|
+
try:
|
|
492
|
+
if self.profile_filename.endswith('.yaml') or\
|
|
493
|
+
self.profile_filename.endswith('.yml'):
|
|
494
|
+
yaml_content = yaml.safe_load(file)
|
|
495
|
+
file_contents = json.dumps(yaml_content)
|
|
496
|
+
profile_contents = json.loads(file_contents)
|
|
497
|
+
else:
|
|
498
|
+
profile_contents = json.load(file)
|
|
499
|
+
except json.JSONDecodeError:
|
|
500
|
+
pass
|
|
501
|
+
return profile_contents
|
|
502
|
+
|
|
503
|
+
def _setup_token_and_token_parts(self, base_url, api_key, byosnap_id) -> None:
|
|
504
|
+
'''
|
|
505
|
+
Setup the token and token parts for publishing and syncing
|
|
506
|
+
'''
|
|
507
|
+
self.token: Union[str, None] = get_composite_token(
|
|
508
|
+
base_url, api_key,
|
|
509
|
+
'byosnap', {'service_id': byosnap_id}
|
|
510
|
+
)
|
|
511
|
+
self.token_parts: Union[list, None] = ByoSnap._get_token_values(
|
|
512
|
+
self.token) if self.token is not None else None
|
|
513
|
+
|
|
514
|
+
def _setup_and_validate_byosnap_profile_data(self) -> None:
|
|
515
|
+
"""
|
|
516
|
+
Pre-Override Validator
|
|
517
|
+
"""
|
|
518
|
+
# Check dependencies
|
|
519
|
+
if self.path is None and self.resources_path is None:
|
|
520
|
+
snapctl_error(
|
|
521
|
+
message='Either the path or resources path is required ' +
|
|
522
|
+
'to import the BYOSnap profile.',
|
|
523
|
+
code=SNAPCTL_INPUT_ERROR
|
|
524
|
+
)
|
|
525
|
+
base_path = self.resources_path if self.resources_path else self.path
|
|
526
|
+
# Publish and Publish version
|
|
527
|
+
if not self.profile_filename:
|
|
528
|
+
self.profile_filename = ByoSnap.DEFAULT_PROFILE_NAME_JSON
|
|
213
529
|
else:
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
530
|
+
if not self.profile_filename.endswith('.json') and \
|
|
531
|
+
not self.profile_filename.endswith('.yaml') and \
|
|
532
|
+
not self.profile_filename.endswith('.yml'):
|
|
533
|
+
snapctl_error(
|
|
534
|
+
message='Invalid BYOSnap profile file. Please check the file extension' +
|
|
535
|
+
' and ensure it is either .json, .yaml, or .yml',
|
|
536
|
+
code=SNAPCTL_INPUT_ERROR
|
|
537
|
+
)
|
|
538
|
+
self.profile_path = os.path.join(
|
|
539
|
+
base_path, self.profile_filename)
|
|
540
|
+
if not os.path.isfile(self.profile_path):
|
|
220
541
|
snapctl_error(
|
|
221
542
|
"Unable to find " +
|
|
222
|
-
f"{self.
|
|
543
|
+
f"{self.profile_filename} at path {base_path}",
|
|
223
544
|
SNAPCTL_INPUT_ERROR)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
self.
|
|
545
|
+
profile_data_obj = self._get_profile_contents()
|
|
546
|
+
if not profile_data_obj:
|
|
547
|
+
snapctl_error(
|
|
548
|
+
message='Invalid BYOSnap profile JSON. Please check the JSON structure',
|
|
549
|
+
code=SNAPCTL_INPUT_ERROR
|
|
550
|
+
)
|
|
551
|
+
# IMPORTANT: This is where the profile data is set and validated
|
|
552
|
+
self.profile_data = profile_data_obj
|
|
553
|
+
ByoSnap._validate_byosnap_profile_data(self.profile_data)
|
|
554
|
+
# End: IMPORTANT: This is where the profile data is set
|
|
555
|
+
# Now apply the overrides
|
|
556
|
+
self.name = self.profile_data['name']
|
|
557
|
+
self.desc = self.profile_data['description']
|
|
558
|
+
self.platform_type = self.profile_data['platform']
|
|
559
|
+
self.language = self.profile_data['language']
|
|
560
|
+
self.prefix = self.profile_data['prefix']
|
|
232
561
|
# Setup the final ingress external port
|
|
233
562
|
final_ingress_external_port = {
|
|
234
563
|
'name': 'http',
|
|
235
564
|
'port': None
|
|
236
565
|
}
|
|
237
|
-
if 'http_port' in profile_data:
|
|
566
|
+
if 'http_port' in self.profile_data:
|
|
238
567
|
final_ingress_external_port = {
|
|
239
568
|
'name': 'http',
|
|
240
|
-
'port': profile_data['http_port']
|
|
569
|
+
'port': self.profile_data['http_port']
|
|
241
570
|
}
|
|
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']
|
|
571
|
+
elif 'ingress' in self.profile_data and 'external_port' in self.profile_data['ingress']:
|
|
572
|
+
final_ingress_external_port = self.profile_data['ingress']['external_port']
|
|
246
573
|
self.ingress_external_port = final_ingress_external_port
|
|
247
574
|
# Setup the final ingress internal ports
|
|
248
575
|
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']
|
|
576
|
+
if 'ingress' in self.profile_data and 'internal_ports' in self.profile_data['ingress']:
|
|
577
|
+
final_ingress_internal_ports = self.profile_data['ingress']['internal_ports']
|
|
251
578
|
self.ingress_internal_ports = final_ingress_internal_ports
|
|
252
|
-
self.readiness_path = profile_data['readiness_probe_config']['path']
|
|
579
|
+
self.readiness_path = self.profile_data['readiness_probe_config']['path']
|
|
253
580
|
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)
|
|
581
|
+
self.profile_data['readiness_probe_config']['initial_delay_seconds']
|
|
329
582
|
|
|
330
583
|
def _check_dependencies(self) -> None:
|
|
331
584
|
"""
|
|
@@ -420,7 +673,7 @@ class ByoSnap:
|
|
|
420
673
|
|
|
421
674
|
def _docker_build(self) -> None:
|
|
422
675
|
# Get the data
|
|
423
|
-
# image_tag = f'{self.
|
|
676
|
+
# image_tag = f'{self.byosnap_id}.{self.tag}'
|
|
424
677
|
build_platform = ByoSnap.DEFAULT_BUILD_PLATFORM
|
|
425
678
|
if len(self.token_parts) == 4:
|
|
426
679
|
build_platform = self.token_parts[3]
|
|
@@ -438,7 +691,7 @@ class ByoSnap:
|
|
|
438
691
|
base_path = self.resources_path
|
|
439
692
|
else:
|
|
440
693
|
base_path = self.path
|
|
441
|
-
docker_file_path = os.path.join(base_path, self.
|
|
694
|
+
docker_file_path = os.path.join(base_path, self.docker_filename)
|
|
442
695
|
|
|
443
696
|
# Warning check for architecture specific commands
|
|
444
697
|
info(f'Building on system architecture {sys_platform.machine()}')
|
|
@@ -478,7 +731,7 @@ class ByoSnap:
|
|
|
478
731
|
def _docker_tag(self) -> None:
|
|
479
732
|
# Get the data
|
|
480
733
|
ecr_repo_url = self.token_parts[0]
|
|
481
|
-
image_tag = f'{self.
|
|
734
|
+
image_tag = f'{self.byosnap_id}.{self.tag}'
|
|
482
735
|
full_ecr_repo_url = f'{ecr_repo_url}:{image_tag}'
|
|
483
736
|
progress = Progress(
|
|
484
737
|
SpinnerColumn(),
|
|
@@ -525,7 +778,7 @@ class ByoSnap:
|
|
|
525
778
|
try:
|
|
526
779
|
# Push the image
|
|
527
780
|
ecr_repo_url = self.token_parts[0]
|
|
528
|
-
image_tag = f'{self.
|
|
781
|
+
image_tag = f'{self.byosnap_id}.{self.tag}'
|
|
529
782
|
full_ecr_repo_url = f'{ecr_repo_url}:{image_tag}'
|
|
530
783
|
if platform == "win32":
|
|
531
784
|
response = subprocess.run([
|
|
@@ -580,8 +833,8 @@ class ByoSnap:
|
|
|
580
833
|
|
|
581
834
|
# Public methods
|
|
582
835
|
|
|
583
|
-
#
|
|
584
|
-
def
|
|
836
|
+
# Validate
|
|
837
|
+
def setup_and_validate_input(self) -> None:
|
|
585
838
|
"""
|
|
586
839
|
Validator
|
|
587
840
|
"""
|
|
@@ -596,21 +849,10 @@ class ByoSnap:
|
|
|
596
849
|
f"{', '.join(ByoSnap.SUBCOMMANDS)}.",
|
|
597
850
|
code=SNAPCTL_INPUT_ERROR
|
|
598
851
|
)
|
|
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
852
|
# Validation for subcommands
|
|
613
853
|
if self.subcommand == 'create':
|
|
854
|
+
# Validator
|
|
855
|
+
ByoSnap._validate_byosnap_id(self.byosnap_id)
|
|
614
856
|
if self.name == '':
|
|
615
857
|
snapctl_error(message="Missing name", code=SNAPCTL_INPUT_ERROR)
|
|
616
858
|
if not self.language:
|
|
@@ -629,9 +871,14 @@ class ByoSnap:
|
|
|
629
871
|
code=SNAPCTL_INPUT_ERROR
|
|
630
872
|
)
|
|
631
873
|
elif self.subcommand == 'publish-image':
|
|
874
|
+
# Setup
|
|
875
|
+
self._setup_token_and_token_parts(
|
|
876
|
+
self.base_url, self.api_key, self.byosnap_id)
|
|
877
|
+
# Validator
|
|
632
878
|
if self.token_parts is None:
|
|
633
879
|
snapctl_error('Invalid token. Please reach out to your support team.',
|
|
634
880
|
SNAPCTL_INPUT_ERROR)
|
|
881
|
+
ByoSnap._validate_byosnap_id(self.byosnap_id)
|
|
635
882
|
if not self.tag:
|
|
636
883
|
snapctl_error(
|
|
637
884
|
"Missing required parameter: tag", SNAPCTL_INPUT_ERROR)
|
|
@@ -648,15 +895,20 @@ class ByoSnap:
|
|
|
648
895
|
# Check path
|
|
649
896
|
if self.resources_path:
|
|
650
897
|
docker_file_path = \
|
|
651
|
-
f"{self.resources_path}/{self.
|
|
898
|
+
f"{self.resources_path}/{self.docker_filename}"
|
|
652
899
|
else:
|
|
653
|
-
docker_file_path = f"{self.path}/{self.
|
|
900
|
+
docker_file_path = f"{self.path}/{self.docker_filename}"
|
|
654
901
|
if not self.skip_build and not os.path.isfile(docker_file_path):
|
|
655
902
|
snapctl_error(
|
|
656
903
|
"Unable to find " +
|
|
657
|
-
f"{self.
|
|
904
|
+
f"{self.docker_filename} at path {docker_file_path}",
|
|
658
905
|
SNAPCTL_INPUT_ERROR)
|
|
659
906
|
elif self.subcommand == 'upload-docs':
|
|
907
|
+
# Setup
|
|
908
|
+
self._setup_token_and_token_parts(
|
|
909
|
+
self.base_url, self.api_key, self.byosnap_id)
|
|
910
|
+
# Validator
|
|
911
|
+
ByoSnap._validate_byosnap_id(self.byosnap_id)
|
|
660
912
|
if self.token_parts is None:
|
|
661
913
|
snapctl_error('Invalid token. Please reach out to your support team.',
|
|
662
914
|
SNAPCTL_INPUT_ERROR)
|
|
@@ -673,6 +925,13 @@ class ByoSnap:
|
|
|
673
925
|
SNAPCTL_INPUT_ERROR
|
|
674
926
|
)
|
|
675
927
|
elif self.subcommand == 'publish-version':
|
|
928
|
+
# Setup
|
|
929
|
+
self._setup_token_and_token_parts(
|
|
930
|
+
self.base_url, self.api_key, self.byosnap_id)
|
|
931
|
+
# Setup the profile data
|
|
932
|
+
self._setup_and_validate_byosnap_profile_data()
|
|
933
|
+
# Validator
|
|
934
|
+
ByoSnap._validate_byosnap_id(self.byosnap_id)
|
|
676
935
|
if self.token_parts is None:
|
|
677
936
|
snapctl_error('Invalid token. Please reach out to your support team.',
|
|
678
937
|
SNAPCTL_INPUT_ERROR)
|
|
@@ -686,7 +945,18 @@ class ByoSnap:
|
|
|
686
945
|
f"{ByoSnap.TAG_CHARACTER_LIMIT} characters",
|
|
687
946
|
SNAPCTL_INPUT_ERROR
|
|
688
947
|
)
|
|
948
|
+
if not self.version:
|
|
949
|
+
snapctl_error("Missing version", SNAPCTL_INPUT_ERROR)
|
|
950
|
+
pattern = r'^v\d+\.\d+\.\d+$'
|
|
951
|
+
if not re.match(r'^v\d+\.\d+\.\d+$', self.version):
|
|
952
|
+
snapctl_error("Version should be in the format vX.X.X",
|
|
953
|
+
SNAPCTL_INPUT_ERROR)
|
|
689
954
|
elif self.subcommand == 'update-version':
|
|
955
|
+
# Setup
|
|
956
|
+
self._setup_token_and_token_parts(
|
|
957
|
+
self.base_url, self.api_key, self.byosnap_id)
|
|
958
|
+
# Validator
|
|
959
|
+
ByoSnap._validate_byosnap_id(self.byosnap_id)
|
|
690
960
|
if self.token_parts is None:
|
|
691
961
|
snapctl_error('Invalid token. Please reach out to your support team.',
|
|
692
962
|
SNAPCTL_INPUT_ERROR)
|
|
@@ -709,6 +979,11 @@ class ByoSnap:
|
|
|
709
979
|
code=SNAPCTL_INPUT_ERROR
|
|
710
980
|
)
|
|
711
981
|
elif self.subcommand == 'sync':
|
|
982
|
+
# Setup
|
|
983
|
+
self._setup_token_and_token_parts(
|
|
984
|
+
self.base_url, self.api_key, self.byosnap_id)
|
|
985
|
+
# Validator
|
|
986
|
+
ByoSnap._validate_byosnap_id(self.byosnap_id)
|
|
712
987
|
if self.token_parts is None:
|
|
713
988
|
snapctl_error('Invalid token. Please reach out to your support team.',
|
|
714
989
|
SNAPCTL_INPUT_ERROR)
|
|
@@ -726,20 +1001,27 @@ class ByoSnap:
|
|
|
726
1001
|
# Check path
|
|
727
1002
|
if self.resources_path:
|
|
728
1003
|
docker_file_path = \
|
|
729
|
-
f"{self.resources_path}/{self.
|
|
1004
|
+
f"{self.resources_path}/{self.docker_filename}"
|
|
730
1005
|
else:
|
|
731
|
-
docker_file_path = f"{self.path}/{self.
|
|
1006
|
+
docker_file_path = f"{self.path}/{self.docker_filename}"
|
|
732
1007
|
if not self.skip_build and not os.path.isfile(docker_file_path):
|
|
733
1008
|
snapctl_error(
|
|
734
1009
|
message="Unable to find " +
|
|
735
|
-
f"{self.
|
|
1010
|
+
f"{self.docker_filename} at path {docker_file_path}",
|
|
1011
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
1012
|
+
if not self.snapend_id:
|
|
1013
|
+
snapctl_error(
|
|
1014
|
+
message="Missing required parameter: snapend-id",
|
|
736
1015
|
code=SNAPCTL_INPUT_ERROR)
|
|
737
1016
|
elif self.subcommand == 'publish':
|
|
1017
|
+
# Setup the profile data
|
|
1018
|
+
self._setup_and_validate_byosnap_profile_data()
|
|
1019
|
+
# Validator
|
|
1020
|
+
ByoSnap._validate_byosnap_id(self.byosnap_id)
|
|
738
1021
|
if not self.version:
|
|
739
1022
|
snapctl_error(message="Missing version. Version should be in the format vX.X.X",
|
|
740
1023
|
code=SNAPCTL_INPUT_ERROR)
|
|
741
|
-
|
|
742
|
-
if not re.match(pattern, self.version):
|
|
1024
|
+
if not re.match(r'^v\d+\.\d+\.\d+$', self.version):
|
|
743
1025
|
snapctl_error(message="Version should be in the format vX.X.X",
|
|
744
1026
|
code=SNAPCTL_INPUT_ERROR)
|
|
745
1027
|
if not self.skip_build and not self.path:
|
|
@@ -749,14 +1031,46 @@ class ByoSnap:
|
|
|
749
1031
|
# Check path
|
|
750
1032
|
if self.resources_path:
|
|
751
1033
|
docker_file_path = \
|
|
752
|
-
f"{self.resources_path}/{self.
|
|
1034
|
+
f"{self.resources_path}/{self.docker_filename}"
|
|
753
1035
|
else:
|
|
754
|
-
docker_file_path = f"{self.path}/{self.
|
|
1036
|
+
docker_file_path = f"{self.path}/{self.docker_filename}"
|
|
755
1037
|
if not self.skip_build and not os.path.isfile(docker_file_path):
|
|
756
1038
|
snapctl_error(
|
|
757
1039
|
message="Unable to find " +
|
|
758
|
-
f"{self.
|
|
1040
|
+
f"{self.docker_filename} at path {docker_file_path}",
|
|
1041
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
1042
|
+
|
|
1043
|
+
# Run the overrides
|
|
1044
|
+
elif self.subcommand == 'generate-profile':
|
|
1045
|
+
# Setup
|
|
1046
|
+
# self._setup_token_and_token_parts(
|
|
1047
|
+
# self.base_url, self.api_key, self.byosnap_id)
|
|
1048
|
+
# Validator
|
|
1049
|
+
if not self.out_path:
|
|
1050
|
+
snapctl_error(
|
|
1051
|
+
message='Missing required parameter: out-path. ' +
|
|
1052
|
+
'Path is required for profile generation',
|
|
759
1053
|
code=SNAPCTL_INPUT_ERROR)
|
|
1054
|
+
if not os.path.isdir(self.out_path):
|
|
1055
|
+
snapctl_error(
|
|
1056
|
+
message='Invalid out-path. ' +
|
|
1057
|
+
'Path should be a directory',
|
|
1058
|
+
code=SNAPCTL_INPUT_ERROR)
|
|
1059
|
+
if self.profile_filename is not None:
|
|
1060
|
+
if not self.profile_filename.endswith('.json') and \
|
|
1061
|
+
not self.profile_filename.endswith('.yaml') and \
|
|
1062
|
+
not self.profile_filename.endswith('.yml'):
|
|
1063
|
+
snapctl_error(
|
|
1064
|
+
message='Invalid BYOSnap profile file. Please check the file extension' +
|
|
1065
|
+
' and ensure it is either .json, .yaml, or .yml',
|
|
1066
|
+
code=SNAPCTL_INPUT_ERROR
|
|
1067
|
+
)
|
|
1068
|
+
elif self.subcommand == 'validate-profile':
|
|
1069
|
+
# # Setup
|
|
1070
|
+
# self._setup_token_and_token_parts(
|
|
1071
|
+
# self.base_url, self.api_key, self.byosnap_id)
|
|
1072
|
+
# Setup the profile data
|
|
1073
|
+
self._setup_and_validate_byosnap_profile_data()
|
|
760
1074
|
|
|
761
1075
|
# Basic methods
|
|
762
1076
|
|
|
@@ -812,7 +1126,7 @@ class ByoSnap:
|
|
|
812
1126
|
with open(swagger_file, "rb") as attachment_file:
|
|
813
1127
|
url = (
|
|
814
1128
|
f"{self.base_url}/v1/snapser-api/byosnaps/"
|
|
815
|
-
f"{self.
|
|
1129
|
+
f"{self.byosnap_id}/docs/{self.tag}/openapispec"
|
|
816
1130
|
)
|
|
817
1131
|
test_res = requests.post(
|
|
818
1132
|
url, files={"attachment": attachment_file},
|
|
@@ -841,7 +1155,7 @@ class ByoSnap:
|
|
|
841
1155
|
with open(readme_file, "rb") as attachment_file:
|
|
842
1156
|
url = (
|
|
843
1157
|
f"{self.base_url}/v1/snapser-api/byosnaps/"
|
|
844
|
-
f"{self.
|
|
1158
|
+
f"{self.byosnap_id}/docs/{self.tag}/markdown"
|
|
845
1159
|
)
|
|
846
1160
|
test_res = requests.post(
|
|
847
1161
|
url, files={"attachment": attachment_file},
|
|
@@ -873,7 +1187,7 @@ class ByoSnap:
|
|
|
873
1187
|
with open(file_path, "rb") as attachment_file:
|
|
874
1188
|
url = (
|
|
875
1189
|
f"{self.base_url}/v1/snapser-api/byosnaps/"
|
|
876
|
-
f"{self.
|
|
1190
|
+
f"{self.byosnap_id}/docs/{self.tag}/tools"
|
|
877
1191
|
)
|
|
878
1192
|
test_res = requests.post(
|
|
879
1193
|
url, files={"attachment": attachment_file},
|
|
@@ -911,7 +1225,7 @@ class ByoSnap:
|
|
|
911
1225
|
progress.add_task(description='Creating your snap...', total=None)
|
|
912
1226
|
try:
|
|
913
1227
|
payload = {
|
|
914
|
-
"service_id": self.
|
|
1228
|
+
"service_id": self.byosnap_id,
|
|
915
1229
|
"name": self.name,
|
|
916
1230
|
"description": self.desc,
|
|
917
1231
|
"platform": self.platform_type,
|
|
@@ -997,16 +1311,20 @@ class ByoSnap:
|
|
|
997
1311
|
progress.add_task(
|
|
998
1312
|
description='Publishing your snap...', total=None)
|
|
999
1313
|
try:
|
|
1000
|
-
profile_data =
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1314
|
+
profile_data = self._get_profile_contents()
|
|
1315
|
+
dev_template = None
|
|
1316
|
+
if 'dev_template' in profile_data:
|
|
1317
|
+
dev_template = profile_data['dev_template']
|
|
1318
|
+
stage_template = None
|
|
1319
|
+
if 'stage_template' in profile_data:
|
|
1320
|
+
stage_template = profile_data['stage_template']
|
|
1321
|
+
prod_template = None
|
|
1322
|
+
if 'prod_template' in profile_data:
|
|
1323
|
+
prod_template = profile_data['prod_template']
|
|
1006
1324
|
payload = {
|
|
1007
1325
|
"version": self.version,
|
|
1008
1326
|
"image_tag": self.tag,
|
|
1009
|
-
"base_url": f"{self.prefix}/{self.
|
|
1327
|
+
"base_url": f"{self.prefix}/{self.byosnap_id}",
|
|
1010
1328
|
"ingress": {
|
|
1011
1329
|
"external_port": self.ingress_external_port,
|
|
1012
1330
|
"internal_ports": self.ingress_internal_ports
|
|
@@ -1015,14 +1333,14 @@ class ByoSnap:
|
|
|
1015
1333
|
"path": self.readiness_path,
|
|
1016
1334
|
"initial_delay_seconds": self.readiness_delay
|
|
1017
1335
|
},
|
|
1018
|
-
"dev_template":
|
|
1019
|
-
"stage_template":
|
|
1020
|
-
"prod_template":
|
|
1336
|
+
"dev_template": dev_template,
|
|
1337
|
+
"stage_template": stage_template,
|
|
1338
|
+
"prod_template": prod_template,
|
|
1021
1339
|
# Currently not supported so we are just hardcoding an empty list
|
|
1022
1340
|
"egress": {"ports": []},
|
|
1023
1341
|
}
|
|
1024
1342
|
res = requests.post(
|
|
1025
|
-
f"{self.base_url}/v1/snapser-api/byosnaps/{self.
|
|
1343
|
+
f"{self.base_url}/v1/snapser-api/byosnaps/{self.byosnap_id}/versions",
|
|
1026
1344
|
json=payload, headers={'api-key': self.api_key},
|
|
1027
1345
|
timeout=SERVER_CALL_TIMEOUT
|
|
1028
1346
|
)
|
|
@@ -1079,7 +1397,7 @@ class ByoSnap:
|
|
|
1079
1397
|
'image_tag': self.tag,
|
|
1080
1398
|
}
|
|
1081
1399
|
res = requests.patch(
|
|
1082
|
-
f"{self.base_url}/v1/snapser-api/byosnaps/{self.
|
|
1400
|
+
f"{self.base_url}/v1/snapser-api/byosnaps/{self.byosnap_id}/versions/{self.version}",
|
|
1083
1401
|
json=payload, headers={'api-key': self.api_key},
|
|
1084
1402
|
timeout=SERVER_CALL_TIMEOUT
|
|
1085
1403
|
)
|
|
@@ -1127,7 +1445,7 @@ class ByoSnap:
|
|
|
1127
1445
|
try:
|
|
1128
1446
|
# Attempt to create a BYOSnap but no worries if it fails
|
|
1129
1447
|
payload = {
|
|
1130
|
-
"service_id": self.
|
|
1448
|
+
"service_id": self.byosnap_id,
|
|
1131
1449
|
"name": self.name,
|
|
1132
1450
|
"description": self.desc,
|
|
1133
1451
|
"platform": self.platform_type,
|
|
@@ -1152,7 +1470,7 @@ class ByoSnap:
|
|
|
1152
1470
|
self.tag = self.version
|
|
1153
1471
|
# Setup the token and token parts
|
|
1154
1472
|
self._setup_token_and_token_parts(
|
|
1155
|
-
self.base_url, self.api_key, self.
|
|
1473
|
+
self.base_url, self.api_key, self.byosnap_id)
|
|
1156
1474
|
# Now publish the image
|
|
1157
1475
|
self.publish_image(no_exit=True)
|
|
1158
1476
|
# Now publish the version
|
|
@@ -1172,7 +1490,7 @@ class ByoSnap:
|
|
|
1172
1490
|
self.tag = f'{self.version}-{int(time.time())}'
|
|
1173
1491
|
self.publish_image(no_exit=True)
|
|
1174
1492
|
self.update_version(no_exit=True)
|
|
1175
|
-
byosnap_list: str = f"{self.
|
|
1493
|
+
byosnap_list: str = f"{self.byosnap_id}:{self.version}"
|
|
1176
1494
|
snapend = Snapend(
|
|
1177
1495
|
subcommand='update', base_url=self.base_url, api_key=self.api_key,
|
|
1178
1496
|
snapend_id=self.snapend_id, byosnaps=byosnap_list, blocking=self.blocking
|
|
@@ -1184,3 +1502,52 @@ class ByoSnap:
|
|
|
1184
1502
|
message='Exception: Unable to update a ' +
|
|
1185
1503
|
f' version for your snap. Exception: {e}',
|
|
1186
1504
|
code=SNAPCTL_BYOSNAP_UPDATE_VERSION_ERROR)
|
|
1505
|
+
|
|
1506
|
+
def generate_profile(self, no_exit: bool = False) -> None:
|
|
1507
|
+
"""
|
|
1508
|
+
Generate snapser-byosnap-profile.json
|
|
1509
|
+
"""
|
|
1510
|
+
progress = Progress(
|
|
1511
|
+
SpinnerColumn(),
|
|
1512
|
+
TextColumn("[progress.description]{task.description}"),
|
|
1513
|
+
transient=True,
|
|
1514
|
+
)
|
|
1515
|
+
progress.start()
|
|
1516
|
+
progress.add_task(
|
|
1517
|
+
description='Generating BYOSnap profile...', total=None)
|
|
1518
|
+
try:
|
|
1519
|
+
if self.out_path is not None:
|
|
1520
|
+
file_save_path = os.path.join(
|
|
1521
|
+
self.out_path, self.profile_filename)
|
|
1522
|
+
else:
|
|
1523
|
+
file_save_path = os.path.join(
|
|
1524
|
+
os.getcwd(), self.profile_filename)
|
|
1525
|
+
file_written = ByoSnap._handle_output_file(
|
|
1526
|
+
f"{ByoSnap.PROFILE_FILES_PATH}{
|
|
1527
|
+
self.profile_filename}", file_save_path
|
|
1528
|
+
)
|
|
1529
|
+
if file_written:
|
|
1530
|
+
snapctl_success(
|
|
1531
|
+
message="BYOSNAP Profile generation successful. " +
|
|
1532
|
+
f"{self.profile_filename} saved at {
|
|
1533
|
+
file_save_path}",
|
|
1534
|
+
progress=progress,
|
|
1535
|
+
no_exit=no_exit
|
|
1536
|
+
)
|
|
1537
|
+
return
|
|
1538
|
+
except (IOError, OSError) as file_error:
|
|
1539
|
+
snapctl_error(
|
|
1540
|
+
message=f"File error: {file_error}",
|
|
1541
|
+
code=SNAPCTL_BYOSNAP_GENERATE_PROFILE_ERROR, progress=progress)
|
|
1542
|
+
snapctl_error(
|
|
1543
|
+
message="Failed to generate BYOSNAP Profile",
|
|
1544
|
+
code=SNAPCTL_BYOSNAP_GENERATE_PROFILE_ERROR,
|
|
1545
|
+
progress=progress
|
|
1546
|
+
)
|
|
1547
|
+
|
|
1548
|
+
def validate_profile(self) -> None:
|
|
1549
|
+
'''
|
|
1550
|
+
Validate the profile
|
|
1551
|
+
'''
|
|
1552
|
+
# Note all the validation is already happening in the constructor
|
|
1553
|
+
return snapctl_success(message='BYOSNAP profile validated.')
|