mlrun 1.7.0rc5__py3-none-any.whl → 1.7.0rc7__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 mlrun might be problematic. Click here for more details.
- mlrun/artifacts/base.py +2 -1
- mlrun/artifacts/plots.py +9 -5
- mlrun/common/constants.py +6 -0
- mlrun/common/schemas/__init__.py +2 -0
- mlrun/common/schemas/model_monitoring/__init__.py +4 -0
- mlrun/common/schemas/model_monitoring/constants.py +35 -18
- mlrun/common/schemas/project.py +1 -0
- mlrun/common/types.py +7 -1
- mlrun/config.py +19 -6
- mlrun/data_types/data_types.py +4 -0
- mlrun/datastore/alibaba_oss.py +130 -0
- mlrun/datastore/azure_blob.py +4 -5
- mlrun/datastore/base.py +22 -16
- mlrun/datastore/datastore.py +4 -0
- mlrun/datastore/google_cloud_storage.py +1 -1
- mlrun/datastore/sources.py +7 -7
- mlrun/db/base.py +14 -6
- mlrun/db/factory.py +1 -1
- mlrun/db/httpdb.py +61 -56
- mlrun/db/nopdb.py +3 -0
- mlrun/launcher/__init__.py +1 -1
- mlrun/launcher/base.py +1 -1
- mlrun/launcher/client.py +1 -1
- mlrun/launcher/factory.py +1 -1
- mlrun/launcher/local.py +1 -1
- mlrun/launcher/remote.py +1 -1
- mlrun/model.py +1 -0
- mlrun/model_monitoring/__init__.py +1 -1
- mlrun/model_monitoring/api.py +104 -301
- mlrun/model_monitoring/application.py +21 -21
- mlrun/model_monitoring/applications/histogram_data_drift.py +130 -40
- mlrun/model_monitoring/controller.py +26 -33
- mlrun/model_monitoring/db/__init__.py +16 -0
- mlrun/model_monitoring/{stores → db/stores}/__init__.py +43 -34
- mlrun/model_monitoring/db/stores/base/__init__.py +15 -0
- mlrun/model_monitoring/{stores/model_endpoint_store.py → db/stores/base/store.py} +47 -6
- mlrun/model_monitoring/db/stores/sqldb/__init__.py +13 -0
- mlrun/model_monitoring/db/stores/sqldb/models/__init__.py +49 -0
- mlrun/model_monitoring/{stores → db/stores/sqldb}/models/base.py +76 -3
- mlrun/model_monitoring/db/stores/sqldb/models/mysql.py +68 -0
- mlrun/model_monitoring/{stores → db/stores/sqldb}/models/sqlite.py +13 -1
- mlrun/model_monitoring/db/stores/sqldb/sql_store.py +662 -0
- mlrun/model_monitoring/db/stores/v3io_kv/__init__.py +13 -0
- mlrun/model_monitoring/{stores/kv_model_endpoint_store.py → db/stores/v3io_kv/kv_store.py} +134 -3
- mlrun/model_monitoring/features_drift_table.py +34 -22
- mlrun/model_monitoring/helpers.py +45 -6
- mlrun/model_monitoring/stream_processing.py +43 -9
- mlrun/model_monitoring/tracking_policy.py +7 -1
- mlrun/model_monitoring/writer.py +4 -36
- mlrun/projects/pipelines.py +13 -1
- mlrun/projects/project.py +279 -117
- mlrun/run.py +72 -74
- mlrun/runtimes/__init__.py +35 -0
- mlrun/runtimes/base.py +7 -1
- mlrun/runtimes/nuclio/api_gateway.py +188 -61
- mlrun/runtimes/nuclio/application/__init__.py +15 -0
- mlrun/runtimes/nuclio/application/application.py +283 -0
- mlrun/runtimes/nuclio/application/reverse_proxy.go +87 -0
- mlrun/runtimes/nuclio/function.py +53 -1
- mlrun/runtimes/nuclio/serving.py +28 -32
- mlrun/runtimes/pod.py +27 -1
- mlrun/serving/server.py +4 -6
- mlrun/serving/states.py +41 -33
- mlrun/utils/helpers.py +34 -0
- mlrun/utils/version/version.json +2 -2
- {mlrun-1.7.0rc5.dist-info → mlrun-1.7.0rc7.dist-info}/METADATA +14 -5
- {mlrun-1.7.0rc5.dist-info → mlrun-1.7.0rc7.dist-info}/RECORD +71 -64
- mlrun/model_monitoring/batch.py +0 -974
- mlrun/model_monitoring/stores/models/__init__.py +0 -27
- mlrun/model_monitoring/stores/models/mysql.py +0 -34
- mlrun/model_monitoring/stores/sql_model_endpoint_store.py +0 -382
- {mlrun-1.7.0rc5.dist-info → mlrun-1.7.0rc7.dist-info}/LICENSE +0 -0
- {mlrun-1.7.0rc5.dist-info → mlrun-1.7.0rc7.dist-info}/WHEEL +0 -0
- {mlrun-1.7.0rc5.dist-info → mlrun-1.7.0rc7.dist-info}/entry_points.txt +0 -0
- {mlrun-1.7.0rc5.dist-info → mlrun-1.7.0rc7.dist-info}/top_level.txt +0 -0
|
@@ -12,15 +12,17 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
import base64
|
|
15
|
+
import typing
|
|
15
16
|
from typing import Optional, Union
|
|
16
17
|
from urllib.parse import urljoin
|
|
17
18
|
|
|
18
19
|
import requests
|
|
20
|
+
from requests.auth import HTTPBasicAuth
|
|
19
21
|
|
|
20
22
|
import mlrun
|
|
21
23
|
import mlrun.common.schemas
|
|
22
24
|
|
|
23
|
-
from .function import RemoteRuntime
|
|
25
|
+
from .function import RemoteRuntime, get_fullname
|
|
24
26
|
from .serving import ServingRuntime
|
|
25
27
|
|
|
26
28
|
NUCLIO_API_GATEWAY_AUTHENTICATION_MODE_BASIC_AUTH = "basicAuth"
|
|
@@ -28,6 +30,67 @@ NUCLIO_API_GATEWAY_AUTHENTICATION_MODE_NONE = "none"
|
|
|
28
30
|
PROJECT_NAME_LABEL = "nuclio.io/project-name"
|
|
29
31
|
|
|
30
32
|
|
|
33
|
+
class APIGatewayAuthenticator(typing.Protocol):
|
|
34
|
+
@property
|
|
35
|
+
def authentication_mode(self) -> str:
|
|
36
|
+
return NUCLIO_API_GATEWAY_AUTHENTICATION_MODE_NONE
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_scheme(cls, api_gateway_spec: mlrun.common.schemas.APIGatewaySpec):
|
|
40
|
+
if (
|
|
41
|
+
api_gateway_spec.authenticationMode
|
|
42
|
+
== NUCLIO_API_GATEWAY_AUTHENTICATION_MODE_BASIC_AUTH
|
|
43
|
+
):
|
|
44
|
+
if api_gateway_spec.authentication:
|
|
45
|
+
return BasicAuth(
|
|
46
|
+
username=api_gateway_spec.authentication.get("username", ""),
|
|
47
|
+
password=api_gateway_spec.authentication.get("password", ""),
|
|
48
|
+
)
|
|
49
|
+
else:
|
|
50
|
+
return BasicAuth()
|
|
51
|
+
else:
|
|
52
|
+
return NoneAuth()
|
|
53
|
+
|
|
54
|
+
def to_scheme(
|
|
55
|
+
self,
|
|
56
|
+
) -> Optional[dict[str, Optional[mlrun.common.schemas.APIGatewayBasicAuth]]]:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class NoneAuth(APIGatewayAuthenticator):
|
|
61
|
+
"""
|
|
62
|
+
An API gateway authenticator with no authentication.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class BasicAuth(APIGatewayAuthenticator):
|
|
69
|
+
"""
|
|
70
|
+
An API gateway authenticator with basic authentication.
|
|
71
|
+
|
|
72
|
+
:param username: (str) The username for basic authentication.
|
|
73
|
+
:param password: (str) The password for basic authentication.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(self, username=None, password=None):
|
|
77
|
+
self._username = username
|
|
78
|
+
self._password = password
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def authentication_mode(self) -> str:
|
|
82
|
+
return NUCLIO_API_GATEWAY_AUTHENTICATION_MODE_BASIC_AUTH
|
|
83
|
+
|
|
84
|
+
def to_scheme(
|
|
85
|
+
self,
|
|
86
|
+
) -> Optional[dict[str, Optional[mlrun.common.schemas.APIGatewayBasicAuth]]]:
|
|
87
|
+
return {
|
|
88
|
+
"authentication": mlrun.common.schemas.APIGatewayBasicAuth(
|
|
89
|
+
username=self._username, password=self._password
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
31
94
|
class APIGateway:
|
|
32
95
|
def __init__(
|
|
33
96
|
self,
|
|
@@ -47,22 +110,34 @@ class APIGateway:
|
|
|
47
110
|
],
|
|
48
111
|
description: str = "",
|
|
49
112
|
path: str = "/",
|
|
50
|
-
|
|
51
|
-
str
|
|
52
|
-
] = NUCLIO_API_GATEWAY_AUTHENTICATION_MODE_NONE,
|
|
113
|
+
authentication: Optional[APIGatewayAuthenticator] = NoneAuth(),
|
|
53
114
|
host: Optional[str] = None,
|
|
54
115
|
canary: Optional[list[int]] = None,
|
|
55
|
-
username: Optional[str] = None,
|
|
56
|
-
password: Optional[str] = None,
|
|
57
116
|
):
|
|
117
|
+
"""
|
|
118
|
+
Initialize the APIGateway instance.
|
|
119
|
+
|
|
120
|
+
:param project: The project name
|
|
121
|
+
:param name: The name of the API gateway
|
|
122
|
+
:param functions: The list of functions associated with the API gateway
|
|
123
|
+
Can be a list of function names (["my-func1", "my-func2"])
|
|
124
|
+
or a list or a single entity of
|
|
125
|
+
:py:class:`~mlrun.runtimes.nuclio.function.RemoteRuntime` OR
|
|
126
|
+
:py:class:`~mlrun.runtimes.nuclio.serving.ServingRuntime`
|
|
127
|
+
|
|
128
|
+
:param description: Optional description of the API gateway
|
|
129
|
+
:param path: Optional path of the API gateway, default value is "/"
|
|
130
|
+
:param authentication: The authentication for the API gateway of type
|
|
131
|
+
:py:class:`~mlrun.runtimes.nuclio.api_gateway.BasicAuth`
|
|
132
|
+
:param host: The host of the API gateway (optional). If not set, it will be automatically generated
|
|
133
|
+
:param canary: The canary percents for the API gateway of type list[int]; for instance: [20,80]
|
|
134
|
+
"""
|
|
58
135
|
self.functions = None
|
|
59
136
|
self._validate(
|
|
60
137
|
project=project,
|
|
61
138
|
functions=functions,
|
|
62
139
|
name=name,
|
|
63
140
|
canary=canary,
|
|
64
|
-
username=username,
|
|
65
|
-
password=password,
|
|
66
141
|
)
|
|
67
142
|
self.project = project
|
|
68
143
|
self.name = name
|
|
@@ -70,14 +145,8 @@ class APIGateway:
|
|
|
70
145
|
|
|
71
146
|
self.path = path
|
|
72
147
|
self.description = description
|
|
73
|
-
self.authentication_mode = (
|
|
74
|
-
authentication_mode
|
|
75
|
-
if authentication_mode
|
|
76
|
-
else self._enrich_authentication_mode(username=username, password=password)
|
|
77
|
-
)
|
|
78
148
|
self.canary = canary
|
|
79
|
-
self.
|
|
80
|
-
self._password = password
|
|
149
|
+
self.authentication = authentication
|
|
81
150
|
|
|
82
151
|
def invoke(
|
|
83
152
|
self,
|
|
@@ -86,24 +155,94 @@ class APIGateway:
|
|
|
86
155
|
auth: Optional[tuple[str, str]] = None,
|
|
87
156
|
**kwargs,
|
|
88
157
|
):
|
|
158
|
+
"""
|
|
159
|
+
Invoke the API gateway.
|
|
160
|
+
|
|
161
|
+
:param method: (str, optional) The HTTP method for the invocation.
|
|
162
|
+
:param headers: (dict, optional) The HTTP headers for the invocation.
|
|
163
|
+
:param auth: (Optional[tuple[str, str]], optional) The authentication creds for the invocation if required.
|
|
164
|
+
:param kwargs: (dict) Additional keyword arguments.
|
|
165
|
+
|
|
166
|
+
:return: The response from the API gateway invocation.
|
|
167
|
+
"""
|
|
89
168
|
if not self.invoke_url:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
169
|
+
# try to resolve invoke_url before fail
|
|
170
|
+
self.sync()
|
|
171
|
+
if not self.invoke_url:
|
|
172
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
173
|
+
"Invocation url is not set. Set up gateway's `invoke_url` attribute."
|
|
174
|
+
)
|
|
93
175
|
if (
|
|
94
|
-
self.authentication_mode
|
|
176
|
+
self.authentication.authentication_mode
|
|
95
177
|
== NUCLIO_API_GATEWAY_AUTHENTICATION_MODE_BASIC_AUTH
|
|
96
178
|
and not auth
|
|
97
179
|
):
|
|
98
180
|
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
99
181
|
"API Gateway invocation requires authentication. Please pass credentials"
|
|
100
182
|
)
|
|
101
|
-
if auth:
|
|
102
|
-
headers["Authorization"] = self._generate_basic_auth(*auth)
|
|
103
183
|
return requests.request(
|
|
104
|
-
method=method,
|
|
184
|
+
method=method,
|
|
185
|
+
url=self.invoke_url,
|
|
186
|
+
headers=headers,
|
|
187
|
+
**kwargs,
|
|
188
|
+
auth=HTTPBasicAuth(*auth) if auth else None,
|
|
105
189
|
)
|
|
106
190
|
|
|
191
|
+
def sync(self):
|
|
192
|
+
"""
|
|
193
|
+
Synchronize the API gateway from the server.
|
|
194
|
+
"""
|
|
195
|
+
synced_gateway = mlrun.get_run_db().get_api_gateway(self.name, self.project)
|
|
196
|
+
synced_gateway = self.from_scheme(synced_gateway)
|
|
197
|
+
|
|
198
|
+
self.host = synced_gateway.host
|
|
199
|
+
self.path = synced_gateway.path
|
|
200
|
+
self.authentication = synced_gateway.authentication
|
|
201
|
+
self.functions = synced_gateway.functions
|
|
202
|
+
self.canary = synced_gateway.canary
|
|
203
|
+
self.description = synced_gateway.description
|
|
204
|
+
|
|
205
|
+
def with_basic_auth(self, username: str, password: str):
|
|
206
|
+
"""
|
|
207
|
+
Set basic authentication for the API gateway.
|
|
208
|
+
|
|
209
|
+
:param username: (str) The username for basic authentication.
|
|
210
|
+
:param password: (str) The password for basic authentication.
|
|
211
|
+
"""
|
|
212
|
+
self.authentication = BasicAuth(username=username, password=password)
|
|
213
|
+
|
|
214
|
+
def with_canary(
|
|
215
|
+
self,
|
|
216
|
+
functions: Union[
|
|
217
|
+
list[str],
|
|
218
|
+
list[
|
|
219
|
+
Union[
|
|
220
|
+
RemoteRuntime,
|
|
221
|
+
ServingRuntime,
|
|
222
|
+
]
|
|
223
|
+
],
|
|
224
|
+
],
|
|
225
|
+
canary: list[int],
|
|
226
|
+
):
|
|
227
|
+
"""
|
|
228
|
+
Set canary function for the API gateway
|
|
229
|
+
|
|
230
|
+
:param functions: The list of functions associated with the API gateway
|
|
231
|
+
Can be a list of function names (["my-func1", "my-func2"])
|
|
232
|
+
or a list of nuclio functions of types
|
|
233
|
+
:py:class:`~mlrun.runtimes.nuclio.function.RemoteRuntime` OR
|
|
234
|
+
:py:class:`~mlrun.runtimes.nuclio.serving.ServingRuntime`
|
|
235
|
+
:param canary: The canary percents for the API gateway of type list[int]; for instance: [20,80]
|
|
236
|
+
|
|
237
|
+
"""
|
|
238
|
+
if len(functions) != 2:
|
|
239
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
240
|
+
f"Gateway with canary can be created only with two functions, "
|
|
241
|
+
f"the number of functions passed is {len(functions)}"
|
|
242
|
+
)
|
|
243
|
+
self.functions = self._validate_functions(self.project, functions)
|
|
244
|
+
self.canary = self._validate_canary(canary)
|
|
245
|
+
|
|
107
246
|
@classmethod
|
|
108
247
|
def from_scheme(cls, api_gateway: mlrun.common.schemas.APIGateway):
|
|
109
248
|
project = api_gateway.metadata.labels.get(PROJECT_NAME_LABEL)
|
|
@@ -114,7 +253,7 @@ class APIGateway:
|
|
|
114
253
|
name=api_gateway.spec.name,
|
|
115
254
|
host=api_gateway.spec.host,
|
|
116
255
|
path=api_gateway.spec.path,
|
|
117
|
-
|
|
256
|
+
authentication=APIGatewayAuthenticator.from_scheme(api_gateway.spec),
|
|
118
257
|
functions=functions,
|
|
119
258
|
canary=canary,
|
|
120
259
|
)
|
|
@@ -141,26 +280,26 @@ class APIGateway:
|
|
|
141
280
|
spec=mlrun.common.schemas.APIGatewaySpec(
|
|
142
281
|
name=self.name,
|
|
143
282
|
description=self.description,
|
|
283
|
+
host=self.host,
|
|
144
284
|
path=self.path,
|
|
145
|
-
|
|
146
|
-
self.authentication_mode
|
|
285
|
+
authenticationMode=mlrun.common.schemas.APIGatewayAuthenticationMode.from_str(
|
|
286
|
+
self.authentication.authentication_mode
|
|
147
287
|
),
|
|
148
288
|
upstreams=upstreams,
|
|
149
289
|
),
|
|
150
290
|
)
|
|
151
|
-
|
|
152
|
-
self.authentication_mode
|
|
153
|
-
is NUCLIO_API_GATEWAY_AUTHENTICATION_MODE_BASIC_AUTH
|
|
154
|
-
):
|
|
155
|
-
api_gateway.spec.authentication = mlrun.common.schemas.APIGatewayBasicAuth(
|
|
156
|
-
username=self._username, password=self._password
|
|
157
|
-
)
|
|
291
|
+
api_gateway.spec.authentication = self.authentication.to_scheme()
|
|
158
292
|
return api_gateway
|
|
159
293
|
|
|
160
294
|
@property
|
|
161
295
|
def invoke_url(
|
|
162
296
|
self,
|
|
163
297
|
):
|
|
298
|
+
"""
|
|
299
|
+
Get the invoke URL.
|
|
300
|
+
|
|
301
|
+
:return: (str) The invoke URL.
|
|
302
|
+
"""
|
|
164
303
|
return urljoin(self.host, self.path)
|
|
165
304
|
|
|
166
305
|
def _validate(
|
|
@@ -180,8 +319,6 @@ class APIGateway:
|
|
|
180
319
|
],
|
|
181
320
|
],
|
|
182
321
|
canary: Optional[list[int]] = None,
|
|
183
|
-
username: Optional[str] = None,
|
|
184
|
-
password: Optional[str] = None,
|
|
185
322
|
):
|
|
186
323
|
if not name:
|
|
187
324
|
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
@@ -192,26 +329,23 @@ class APIGateway:
|
|
|
192
329
|
|
|
193
330
|
# validating canary
|
|
194
331
|
if canary:
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if sum(canary) != 100:
|
|
332
|
+
self._validate_canary(canary)
|
|
333
|
+
|
|
334
|
+
def _validate_canary(self, canary: list[int]):
|
|
335
|
+
if len(self.functions) != len(canary):
|
|
336
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
337
|
+
"Function and canary lists lengths do not match"
|
|
338
|
+
)
|
|
339
|
+
for canary_percent in canary:
|
|
340
|
+
if canary_percent < 0 or canary_percent > 100:
|
|
205
341
|
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
206
|
-
"The
|
|
342
|
+
"The percentage value must be in the range from 0 to 100"
|
|
207
343
|
)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if password and not username:
|
|
214
|
-
raise mlrun.errors.MLRunInvalidArgumentError("Username is not specified")
|
|
344
|
+
if sum(canary) != 100:
|
|
345
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
346
|
+
"The sum of canary function percents should be equal to 100"
|
|
347
|
+
)
|
|
348
|
+
return canary
|
|
215
349
|
|
|
216
350
|
@staticmethod
|
|
217
351
|
def _validate_functions(
|
|
@@ -257,17 +391,10 @@ class APIGateway:
|
|
|
257
391
|
f"input function {function_name} "
|
|
258
392
|
f"does not belong to this project"
|
|
259
393
|
)
|
|
260
|
-
|
|
394
|
+
nuclio_name = get_fullname(function_name, project, func.metadata.tag)
|
|
395
|
+
function_names.append(nuclio_name)
|
|
261
396
|
return function_names
|
|
262
397
|
|
|
263
|
-
@staticmethod
|
|
264
|
-
def _enrich_authentication_mode(username, password):
|
|
265
|
-
return (
|
|
266
|
-
NUCLIO_API_GATEWAY_AUTHENTICATION_MODE_NONE
|
|
267
|
-
if username is not None and password is not None
|
|
268
|
-
else NUCLIO_API_GATEWAY_AUTHENTICATION_MODE_BASIC_AUTH
|
|
269
|
-
)
|
|
270
|
-
|
|
271
398
|
@staticmethod
|
|
272
399
|
def _generate_basic_auth(username: str, password: str):
|
|
273
400
|
token = base64.b64encode(f"{username}:{password}".encode()).decode()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copyright 2024 Iguazio
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from .application import ApplicationRuntime
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# Copyright 2024 Iguazio
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
import pathlib
|
|
15
|
+
|
|
16
|
+
import nuclio
|
|
17
|
+
|
|
18
|
+
import mlrun.errors
|
|
19
|
+
from mlrun.common.schemas import AuthInfo
|
|
20
|
+
from mlrun.runtimes import RemoteRuntime
|
|
21
|
+
from mlrun.runtimes.nuclio import min_nuclio_versions
|
|
22
|
+
from mlrun.runtimes.nuclio.function import NuclioSpec, NuclioStatus
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ApplicationSpec(NuclioSpec):
|
|
26
|
+
_dict_fields = NuclioSpec._dict_fields + [
|
|
27
|
+
"internal_application_port",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
command=None,
|
|
33
|
+
args=None,
|
|
34
|
+
image=None,
|
|
35
|
+
mode=None,
|
|
36
|
+
entry_points=None,
|
|
37
|
+
description=None,
|
|
38
|
+
replicas=None,
|
|
39
|
+
min_replicas=None,
|
|
40
|
+
max_replicas=None,
|
|
41
|
+
volumes=None,
|
|
42
|
+
volume_mounts=None,
|
|
43
|
+
env=None,
|
|
44
|
+
resources=None,
|
|
45
|
+
config=None,
|
|
46
|
+
base_spec=None,
|
|
47
|
+
no_cache=None,
|
|
48
|
+
source=None,
|
|
49
|
+
image_pull_policy=None,
|
|
50
|
+
function_kind=None,
|
|
51
|
+
build=None,
|
|
52
|
+
service_account=None,
|
|
53
|
+
readiness_timeout=None,
|
|
54
|
+
readiness_timeout_before_failure=None,
|
|
55
|
+
default_handler=None,
|
|
56
|
+
node_name=None,
|
|
57
|
+
node_selector=None,
|
|
58
|
+
affinity=None,
|
|
59
|
+
disable_auto_mount=False,
|
|
60
|
+
priority_class_name=None,
|
|
61
|
+
pythonpath=None,
|
|
62
|
+
workdir=None,
|
|
63
|
+
image_pull_secret=None,
|
|
64
|
+
tolerations=None,
|
|
65
|
+
preemption_mode=None,
|
|
66
|
+
security_context=None,
|
|
67
|
+
service_type=None,
|
|
68
|
+
add_templated_ingress_host_mode=None,
|
|
69
|
+
clone_target_dir=None,
|
|
70
|
+
state_thresholds=None,
|
|
71
|
+
disable_default_http_trigger=None,
|
|
72
|
+
internal_application_port=None,
|
|
73
|
+
):
|
|
74
|
+
super().__init__(
|
|
75
|
+
command=command,
|
|
76
|
+
args=args,
|
|
77
|
+
image=image,
|
|
78
|
+
mode=mode,
|
|
79
|
+
entry_points=entry_points,
|
|
80
|
+
description=description,
|
|
81
|
+
replicas=replicas,
|
|
82
|
+
min_replicas=min_replicas,
|
|
83
|
+
max_replicas=max_replicas,
|
|
84
|
+
volumes=volumes,
|
|
85
|
+
volume_mounts=volume_mounts,
|
|
86
|
+
env=env,
|
|
87
|
+
resources=resources,
|
|
88
|
+
config=config,
|
|
89
|
+
base_spec=base_spec,
|
|
90
|
+
no_cache=no_cache,
|
|
91
|
+
source=source,
|
|
92
|
+
image_pull_policy=image_pull_policy,
|
|
93
|
+
function_kind=function_kind,
|
|
94
|
+
build=build,
|
|
95
|
+
service_account=service_account,
|
|
96
|
+
readiness_timeout=readiness_timeout,
|
|
97
|
+
readiness_timeout_before_failure=readiness_timeout_before_failure,
|
|
98
|
+
default_handler=default_handler,
|
|
99
|
+
node_name=node_name,
|
|
100
|
+
node_selector=node_selector,
|
|
101
|
+
affinity=affinity,
|
|
102
|
+
disable_auto_mount=disable_auto_mount,
|
|
103
|
+
priority_class_name=priority_class_name,
|
|
104
|
+
pythonpath=pythonpath,
|
|
105
|
+
workdir=workdir,
|
|
106
|
+
image_pull_secret=image_pull_secret,
|
|
107
|
+
tolerations=tolerations,
|
|
108
|
+
preemption_mode=preemption_mode,
|
|
109
|
+
security_context=security_context,
|
|
110
|
+
service_type=service_type,
|
|
111
|
+
add_templated_ingress_host_mode=add_templated_ingress_host_mode,
|
|
112
|
+
clone_target_dir=clone_target_dir,
|
|
113
|
+
state_thresholds=state_thresholds,
|
|
114
|
+
disable_default_http_trigger=disable_default_http_trigger,
|
|
115
|
+
)
|
|
116
|
+
self.internal_application_port = internal_application_port or 8080
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def internal_application_port(self):
|
|
120
|
+
return self._internal_application_port
|
|
121
|
+
|
|
122
|
+
@internal_application_port.setter
|
|
123
|
+
def internal_application_port(self, port):
|
|
124
|
+
port = int(port)
|
|
125
|
+
if port < 0 or port > 65535:
|
|
126
|
+
raise ValueError("Port must be in the range 0-65535")
|
|
127
|
+
self._internal_application_port = port
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class ApplicationStatus(NuclioStatus):
|
|
131
|
+
def __init__(
|
|
132
|
+
self,
|
|
133
|
+
state=None,
|
|
134
|
+
nuclio_name=None,
|
|
135
|
+
address=None,
|
|
136
|
+
internal_invocation_urls=None,
|
|
137
|
+
external_invocation_urls=None,
|
|
138
|
+
build_pod=None,
|
|
139
|
+
container_image=None,
|
|
140
|
+
application_image=None,
|
|
141
|
+
sidecar_name=None,
|
|
142
|
+
):
|
|
143
|
+
super().__init__(
|
|
144
|
+
state=state,
|
|
145
|
+
nuclio_name=nuclio_name,
|
|
146
|
+
address=address,
|
|
147
|
+
internal_invocation_urls=internal_invocation_urls,
|
|
148
|
+
external_invocation_urls=external_invocation_urls,
|
|
149
|
+
build_pod=build_pod,
|
|
150
|
+
container_image=container_image,
|
|
151
|
+
)
|
|
152
|
+
self.application_image = application_image or None
|
|
153
|
+
self.sidecar_name = sidecar_name or None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class ApplicationRuntime(RemoteRuntime):
|
|
157
|
+
kind = "application"
|
|
158
|
+
|
|
159
|
+
@min_nuclio_versions("1.12.7")
|
|
160
|
+
def __init__(self, spec=None, metadata=None):
|
|
161
|
+
super().__init__(spec=spec, metadata=metadata)
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def spec(self) -> ApplicationSpec:
|
|
165
|
+
return self._spec
|
|
166
|
+
|
|
167
|
+
@spec.setter
|
|
168
|
+
def spec(self, spec):
|
|
169
|
+
self._spec = self._verify_dict(spec, "spec", ApplicationSpec)
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def status(self) -> ApplicationStatus:
|
|
173
|
+
return self._status
|
|
174
|
+
|
|
175
|
+
@status.setter
|
|
176
|
+
def status(self, status):
|
|
177
|
+
self._status = self._verify_dict(status, "status", ApplicationStatus)
|
|
178
|
+
|
|
179
|
+
def set_internal_application_port(self, port: int):
|
|
180
|
+
self.spec.internal_application_port = port
|
|
181
|
+
|
|
182
|
+
def pre_deploy_validation(self):
|
|
183
|
+
super().pre_deploy_validation()
|
|
184
|
+
if not self.spec.config.get("spec.sidecars"):
|
|
185
|
+
raise mlrun.errors.MLRunBadRequestError(
|
|
186
|
+
"Application spec must include a sidecar configuration"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
sidecars = self.spec.config["spec.sidecars"]
|
|
190
|
+
for sidecar in sidecars:
|
|
191
|
+
if not sidecar.get("image"):
|
|
192
|
+
raise mlrun.errors.MLRunBadRequestError(
|
|
193
|
+
"Application sidecar spec must include an image"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if not sidecar.get("ports"):
|
|
197
|
+
raise mlrun.errors.MLRunBadRequestError(
|
|
198
|
+
"Application sidecar spec must include at least one port"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
ports = sidecar["ports"]
|
|
202
|
+
for port in ports:
|
|
203
|
+
if not port.get("containerPort"):
|
|
204
|
+
raise mlrun.errors.MLRunBadRequestError(
|
|
205
|
+
"Application sidecar port spec must include a containerPort"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if not port.get("name"):
|
|
209
|
+
raise mlrun.errors.MLRunBadRequestError(
|
|
210
|
+
"Application sidecar port spec must include a name"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
if not sidecar.get("command") and sidecar.get("args"):
|
|
214
|
+
raise mlrun.errors.MLRunBadRequestError(
|
|
215
|
+
"Application sidecar spec must include a command if args are provided"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def deploy(
|
|
219
|
+
self,
|
|
220
|
+
project="",
|
|
221
|
+
tag="",
|
|
222
|
+
verbose=False,
|
|
223
|
+
auth_info: AuthInfo = None,
|
|
224
|
+
builder_env: dict = None,
|
|
225
|
+
force_build: bool = False,
|
|
226
|
+
):
|
|
227
|
+
self._ensure_reverse_proxy_configurations()
|
|
228
|
+
self._configure_application_sidecar()
|
|
229
|
+
super().deploy(
|
|
230
|
+
project,
|
|
231
|
+
tag,
|
|
232
|
+
verbose,
|
|
233
|
+
auth_info,
|
|
234
|
+
builder_env,
|
|
235
|
+
force_build,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
def _ensure_reverse_proxy_configurations(self):
|
|
239
|
+
if self.spec.build.functionSourceCode or self.status.container_image:
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
filename, handler = ApplicationRuntime.get_filename_and_handler()
|
|
243
|
+
name, spec, code = nuclio.build_file(
|
|
244
|
+
filename,
|
|
245
|
+
name=self.metadata.name,
|
|
246
|
+
handler=handler,
|
|
247
|
+
)
|
|
248
|
+
self.spec.function_handler = mlrun.utils.get_in(spec, "spec.handler")
|
|
249
|
+
self.spec.build.functionSourceCode = mlrun.utils.get_in(
|
|
250
|
+
spec, "spec.build.functionSourceCode"
|
|
251
|
+
)
|
|
252
|
+
self.spec.nuclio_runtime = mlrun.utils.get_in(spec, "spec.runtime")
|
|
253
|
+
|
|
254
|
+
def _configure_application_sidecar(self):
|
|
255
|
+
# Save the application image in the status to allow overriding it with the reverse proxy entry point
|
|
256
|
+
if self.spec.image and (
|
|
257
|
+
not self.status.application_image
|
|
258
|
+
or self.spec.image != self.status.container_image
|
|
259
|
+
):
|
|
260
|
+
self.status.application_image = self.spec.image
|
|
261
|
+
self.spec.image = ""
|
|
262
|
+
|
|
263
|
+
if self.status.container_image:
|
|
264
|
+
self.from_image(self.status.container_image)
|
|
265
|
+
# nuclio implementation detail - when providing the image and emptying out the source code,
|
|
266
|
+
# nuclio skips rebuilding the image and simply takes the prebuilt image
|
|
267
|
+
self.spec.build.functionSourceCode = ""
|
|
268
|
+
|
|
269
|
+
self.status.sidecar_name = f"{self.metadata.name}-sidecar"
|
|
270
|
+
self.with_sidecar(
|
|
271
|
+
name=self.status.sidecar_name,
|
|
272
|
+
image=self.status.application_image,
|
|
273
|
+
ports=self.spec.internal_application_port,
|
|
274
|
+
command=self.spec.command,
|
|
275
|
+
args=self.spec.args,
|
|
276
|
+
)
|
|
277
|
+
self.set_env("SIDECAR_PORT", self.spec.internal_application_port)
|
|
278
|
+
self.set_env("SIDECAR_HOST", "http://localhost")
|
|
279
|
+
|
|
280
|
+
@classmethod
|
|
281
|
+
def get_filename_and_handler(cls) -> (str, str):
|
|
282
|
+
reverse_proxy_file_path = pathlib.Path(__file__).parent / "reverse_proxy.go"
|
|
283
|
+
return str(reverse_proxy_file_path), "Handler"
|