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.

Files changed (75) hide show
  1. mlrun/artifacts/base.py +2 -1
  2. mlrun/artifacts/plots.py +9 -5
  3. mlrun/common/constants.py +6 -0
  4. mlrun/common/schemas/__init__.py +2 -0
  5. mlrun/common/schemas/model_monitoring/__init__.py +4 -0
  6. mlrun/common/schemas/model_monitoring/constants.py +35 -18
  7. mlrun/common/schemas/project.py +1 -0
  8. mlrun/common/types.py +7 -1
  9. mlrun/config.py +19 -6
  10. mlrun/data_types/data_types.py +4 -0
  11. mlrun/datastore/alibaba_oss.py +130 -0
  12. mlrun/datastore/azure_blob.py +4 -5
  13. mlrun/datastore/base.py +22 -16
  14. mlrun/datastore/datastore.py +4 -0
  15. mlrun/datastore/google_cloud_storage.py +1 -1
  16. mlrun/datastore/sources.py +7 -7
  17. mlrun/db/base.py +14 -6
  18. mlrun/db/factory.py +1 -1
  19. mlrun/db/httpdb.py +61 -56
  20. mlrun/db/nopdb.py +3 -0
  21. mlrun/launcher/__init__.py +1 -1
  22. mlrun/launcher/base.py +1 -1
  23. mlrun/launcher/client.py +1 -1
  24. mlrun/launcher/factory.py +1 -1
  25. mlrun/launcher/local.py +1 -1
  26. mlrun/launcher/remote.py +1 -1
  27. mlrun/model.py +1 -0
  28. mlrun/model_monitoring/__init__.py +1 -1
  29. mlrun/model_monitoring/api.py +104 -301
  30. mlrun/model_monitoring/application.py +21 -21
  31. mlrun/model_monitoring/applications/histogram_data_drift.py +130 -40
  32. mlrun/model_monitoring/controller.py +26 -33
  33. mlrun/model_monitoring/db/__init__.py +16 -0
  34. mlrun/model_monitoring/{stores → db/stores}/__init__.py +43 -34
  35. mlrun/model_monitoring/db/stores/base/__init__.py +15 -0
  36. mlrun/model_monitoring/{stores/model_endpoint_store.py → db/stores/base/store.py} +47 -6
  37. mlrun/model_monitoring/db/stores/sqldb/__init__.py +13 -0
  38. mlrun/model_monitoring/db/stores/sqldb/models/__init__.py +49 -0
  39. mlrun/model_monitoring/{stores → db/stores/sqldb}/models/base.py +76 -3
  40. mlrun/model_monitoring/db/stores/sqldb/models/mysql.py +68 -0
  41. mlrun/model_monitoring/{stores → db/stores/sqldb}/models/sqlite.py +13 -1
  42. mlrun/model_monitoring/db/stores/sqldb/sql_store.py +662 -0
  43. mlrun/model_monitoring/db/stores/v3io_kv/__init__.py +13 -0
  44. mlrun/model_monitoring/{stores/kv_model_endpoint_store.py → db/stores/v3io_kv/kv_store.py} +134 -3
  45. mlrun/model_monitoring/features_drift_table.py +34 -22
  46. mlrun/model_monitoring/helpers.py +45 -6
  47. mlrun/model_monitoring/stream_processing.py +43 -9
  48. mlrun/model_monitoring/tracking_policy.py +7 -1
  49. mlrun/model_monitoring/writer.py +4 -36
  50. mlrun/projects/pipelines.py +13 -1
  51. mlrun/projects/project.py +279 -117
  52. mlrun/run.py +72 -74
  53. mlrun/runtimes/__init__.py +35 -0
  54. mlrun/runtimes/base.py +7 -1
  55. mlrun/runtimes/nuclio/api_gateway.py +188 -61
  56. mlrun/runtimes/nuclio/application/__init__.py +15 -0
  57. mlrun/runtimes/nuclio/application/application.py +283 -0
  58. mlrun/runtimes/nuclio/application/reverse_proxy.go +87 -0
  59. mlrun/runtimes/nuclio/function.py +53 -1
  60. mlrun/runtimes/nuclio/serving.py +28 -32
  61. mlrun/runtimes/pod.py +27 -1
  62. mlrun/serving/server.py +4 -6
  63. mlrun/serving/states.py +41 -33
  64. mlrun/utils/helpers.py +34 -0
  65. mlrun/utils/version/version.json +2 -2
  66. {mlrun-1.7.0rc5.dist-info → mlrun-1.7.0rc7.dist-info}/METADATA +14 -5
  67. {mlrun-1.7.0rc5.dist-info → mlrun-1.7.0rc7.dist-info}/RECORD +71 -64
  68. mlrun/model_monitoring/batch.py +0 -974
  69. mlrun/model_monitoring/stores/models/__init__.py +0 -27
  70. mlrun/model_monitoring/stores/models/mysql.py +0 -34
  71. mlrun/model_monitoring/stores/sql_model_endpoint_store.py +0 -382
  72. {mlrun-1.7.0rc5.dist-info → mlrun-1.7.0rc7.dist-info}/LICENSE +0 -0
  73. {mlrun-1.7.0rc5.dist-info → mlrun-1.7.0rc7.dist-info}/WHEEL +0 -0
  74. {mlrun-1.7.0rc5.dist-info → mlrun-1.7.0rc7.dist-info}/entry_points.txt +0 -0
  75. {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
- authentication_mode: Optional[
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._username = username
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
- raise mlrun.errors.MLRunInvalidArgumentError(
91
- "Invocation url is not set. Set up gateway's `invoke_url` attribute."
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, url=self.invoke_url, headers=headers, **kwargs
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
- authentication_mode=str(api_gateway.spec.authenticationMode),
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
- authentication_mode=mlrun.common.schemas.APIGatewayAuthenticationMode.from_str(
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
- if (
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
- if len(self.functions) != len(canary):
196
- raise mlrun.errors.MLRunInvalidArgumentError(
197
- "Function and canary lists lengths do not match"
198
- )
199
- for canary_percent in canary:
200
- if canary_percent < 0 or canary_percent > 100:
201
- raise mlrun.errors.MLRunInvalidArgumentError(
202
- "The percentage value must be in the range from 0 to 100"
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 sum of canary function percents should be equal to 100"
342
+ "The percentage value must be in the range from 0 to 100"
207
343
  )
208
-
209
- # validating auth
210
- if username and not password:
211
- raise mlrun.errors.MLRunInvalidArgumentError("Password is not specified")
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
- function_names.append(func.uri)
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"