mlrun 1.7.0rc39__py3-none-any.whl → 1.7.0rc41__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/common/constants.py +3 -0
- mlrun/common/helpers.py +0 -1
- mlrun/common/schemas/model_monitoring/model_endpoints.py +0 -1
- mlrun/config.py +1 -1
- mlrun/data_types/to_pandas.py +9 -9
- mlrun/datastore/alibaba_oss.py +1 -0
- mlrun/datastore/azure_blob.py +1 -6
- mlrun/datastore/base.py +12 -0
- mlrun/datastore/dbfs_store.py +1 -5
- mlrun/datastore/filestore.py +1 -3
- mlrun/datastore/google_cloud_storage.py +1 -9
- mlrun/datastore/redis.py +1 -0
- mlrun/datastore/s3.py +1 -0
- mlrun/datastore/storeytargets.py +147 -0
- mlrun/datastore/targets.py +67 -69
- mlrun/datastore/v3io.py +1 -0
- mlrun/model_monitoring/api.py +1 -2
- mlrun/model_monitoring/applications/_application_steps.py +25 -43
- mlrun/model_monitoring/applications/context.py +206 -70
- mlrun/model_monitoring/controller.py +0 -1
- mlrun/model_monitoring/db/stores/sqldb/sql_store.py +17 -8
- mlrun/model_monitoring/db/stores/v3io_kv/kv_store.py +14 -4
- mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +11 -3
- mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +35 -23
- mlrun/model_monitoring/helpers.py +38 -1
- mlrun/model_monitoring/stream_processing.py +8 -26
- mlrun/projects/project.py +17 -16
- mlrun/runtimes/nuclio/api_gateway.py +9 -0
- mlrun/runtimes/nuclio/application/application.py +131 -55
- mlrun/runtimes/nuclio/function.py +4 -10
- mlrun/runtimes/nuclio/serving.py +2 -2
- mlrun/runtimes/utils.py +16 -0
- mlrun/serving/routers.py +1 -1
- mlrun/serving/server.py +19 -5
- mlrun/serving/states.py +8 -0
- mlrun/serving/v2_serving.py +34 -26
- mlrun/utils/helpers.py +12 -2
- mlrun/utils/version/version.json +2 -2
- {mlrun-1.7.0rc39.dist-info → mlrun-1.7.0rc41.dist-info}/METADATA +2 -2
- {mlrun-1.7.0rc39.dist-info → mlrun-1.7.0rc41.dist-info}/RECORD +44 -43
- {mlrun-1.7.0rc39.dist-info → mlrun-1.7.0rc41.dist-info}/WHEEL +1 -1
- {mlrun-1.7.0rc39.dist-info → mlrun-1.7.0rc41.dist-info}/LICENSE +0 -0
- {mlrun-1.7.0rc39.dist-info → mlrun-1.7.0rc41.dist-info}/entry_points.txt +0 -0
- {mlrun-1.7.0rc39.dist-info → mlrun-1.7.0rc41.dist-info}/top_level.txt +0 -0
|
@@ -24,6 +24,7 @@ import mlrun.common.model_monitoring
|
|
|
24
24
|
import mlrun.common.schemas.model_monitoring as mm_schemas
|
|
25
25
|
import mlrun.feature_store.steps
|
|
26
26
|
import mlrun.utils.v3io_clients
|
|
27
|
+
from mlrun.common.schemas import EventFieldType
|
|
27
28
|
from mlrun.model_monitoring.db import TSDBConnector
|
|
28
29
|
from mlrun.model_monitoring.helpers import get_invocations_fqn
|
|
29
30
|
from mlrun.utils import logger
|
|
@@ -64,14 +65,17 @@ class V3IOTSDBConnector(TSDBConnector):
|
|
|
64
65
|
self.container = container
|
|
65
66
|
|
|
66
67
|
self.v3io_framesd = v3io_framesd or mlrun.mlconf.v3io_framesd
|
|
67
|
-
self._frames_client: v3io_frames.client.ClientBase =
|
|
68
|
-
self._get_v3io_frames_client(self.container)
|
|
69
|
-
)
|
|
70
|
-
|
|
68
|
+
self._frames_client: Optional[v3io_frames.client.ClientBase] = None
|
|
71
69
|
self._init_tables_path()
|
|
70
|
+
self._create_table = create_table
|
|
72
71
|
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
@property
|
|
73
|
+
def frames_client(self) -> v3io_frames.client.ClientBase:
|
|
74
|
+
if not self._frames_client:
|
|
75
|
+
self._frames_client = self._get_v3io_frames_client(self.container)
|
|
76
|
+
if self._create_table:
|
|
77
|
+
self.create_tables()
|
|
78
|
+
return self._frames_client
|
|
75
79
|
|
|
76
80
|
def _init_tables_path(self):
|
|
77
81
|
self.tables = {}
|
|
@@ -151,7 +155,7 @@ class V3IOTSDBConnector(TSDBConnector):
|
|
|
151
155
|
for table_name in application_tables:
|
|
152
156
|
logger.info("Creating table in V3IO TSDB", table_name=table_name)
|
|
153
157
|
table = self.tables[table_name]
|
|
154
|
-
self.
|
|
158
|
+
self.frames_client.create(
|
|
155
159
|
backend=_TSDB_BE,
|
|
156
160
|
table=table,
|
|
157
161
|
if_exists=v3io_frames.IGNORE,
|
|
@@ -161,8 +165,9 @@ class V3IOTSDBConnector(TSDBConnector):
|
|
|
161
165
|
def apply_monitoring_stream_steps(
|
|
162
166
|
self,
|
|
163
167
|
graph,
|
|
164
|
-
tsdb_batching_max_events: int =
|
|
165
|
-
tsdb_batching_timeout_secs: int =
|
|
168
|
+
tsdb_batching_max_events: int = 1000,
|
|
169
|
+
tsdb_batching_timeout_secs: int = 30,
|
|
170
|
+
sample_window: int = 10,
|
|
166
171
|
):
|
|
167
172
|
"""
|
|
168
173
|
Apply TSDB steps on the provided monitoring graph. Throughout these steps, the graph stores live data of
|
|
@@ -173,6 +178,7 @@ class V3IOTSDBConnector(TSDBConnector):
|
|
|
173
178
|
- endpoint_features (Prediction and feature names and values)
|
|
174
179
|
- custom_metrics (user-defined metrics)
|
|
175
180
|
"""
|
|
181
|
+
|
|
176
182
|
# Write latency per prediction, labeled by endpoint ID only
|
|
177
183
|
graph.add_step(
|
|
178
184
|
"storey.TSDBTarget",
|
|
@@ -197,17 +203,23 @@ class V3IOTSDBConnector(TSDBConnector):
|
|
|
197
203
|
key=mm_schemas.EventFieldType.ENDPOINT_ID,
|
|
198
204
|
)
|
|
199
205
|
|
|
206
|
+
# Emits the event in window size of events based on sample_window size (10 by default)
|
|
207
|
+
graph.add_step(
|
|
208
|
+
"storey.steps.SampleWindow",
|
|
209
|
+
name="sample",
|
|
210
|
+
after="Rename",
|
|
211
|
+
window_size=sample_window,
|
|
212
|
+
key=EventFieldType.ENDPOINT_ID,
|
|
213
|
+
)
|
|
214
|
+
|
|
200
215
|
# Before writing data to TSDB, create dictionary of 2-3 dictionaries that contains
|
|
201
216
|
# stats and details about the events
|
|
202
217
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
apply_process_before_tsdb()
|
|
218
|
+
graph.add_step(
|
|
219
|
+
"mlrun.model_monitoring.db.tsdb.v3io.stream_graph_steps.ProcessBeforeTSDB",
|
|
220
|
+
name="ProcessBeforeTSDB",
|
|
221
|
+
after="sample",
|
|
222
|
+
)
|
|
211
223
|
|
|
212
224
|
# Unpacked keys from each dictionary and write to TSDB target
|
|
213
225
|
def apply_filter_and_unpacked_keys(name, keys):
|
|
@@ -273,8 +285,8 @@ class V3IOTSDBConnector(TSDBConnector):
|
|
|
273
285
|
def handle_model_error(
|
|
274
286
|
self,
|
|
275
287
|
graph,
|
|
276
|
-
tsdb_batching_max_events: int =
|
|
277
|
-
tsdb_batching_timeout_secs: int =
|
|
288
|
+
tsdb_batching_max_events: int = 1000,
|
|
289
|
+
tsdb_batching_timeout_secs: int = 30,
|
|
278
290
|
**kwargs,
|
|
279
291
|
) -> None:
|
|
280
292
|
graph.add_step(
|
|
@@ -333,7 +345,7 @@ class V3IOTSDBConnector(TSDBConnector):
|
|
|
333
345
|
raise ValueError(f"Invalid {kind = }")
|
|
334
346
|
|
|
335
347
|
try:
|
|
336
|
-
self.
|
|
348
|
+
self.frames_client.write(
|
|
337
349
|
backend=_TSDB_BE,
|
|
338
350
|
table=table,
|
|
339
351
|
dfs=pd.DataFrame.from_records([event]),
|
|
@@ -360,7 +372,7 @@ class V3IOTSDBConnector(TSDBConnector):
|
|
|
360
372
|
tables = mm_schemas.V3IOTSDBTables.list()
|
|
361
373
|
for table_to_delete in tables:
|
|
362
374
|
try:
|
|
363
|
-
self.
|
|
375
|
+
self.frames_client.delete(backend=_TSDB_BE, table=table_to_delete)
|
|
364
376
|
except v3io_frames.DeleteError as e:
|
|
365
377
|
logger.warning(
|
|
366
378
|
f"Failed to delete TSDB table '{table}'",
|
|
@@ -476,7 +488,7 @@ class V3IOTSDBConnector(TSDBConnector):
|
|
|
476
488
|
aggregators = ",".join(agg_funcs) if agg_funcs else None
|
|
477
489
|
table_path = self.tables[table]
|
|
478
490
|
try:
|
|
479
|
-
df = self.
|
|
491
|
+
df = self.frames_client.read(
|
|
480
492
|
backend=_TSDB_BE,
|
|
481
493
|
table=table_path,
|
|
482
494
|
start=start,
|
|
@@ -579,7 +591,7 @@ class V3IOTSDBConnector(TSDBConnector):
|
|
|
579
591
|
|
|
580
592
|
logger.debug("Querying V3IO TSDB", query=query)
|
|
581
593
|
|
|
582
|
-
df: pd.DataFrame = self.
|
|
594
|
+
df: pd.DataFrame = self.frames_client.read(
|
|
583
595
|
backend=_TSDB_BE,
|
|
584
596
|
start=start,
|
|
585
597
|
end=end,
|
|
@@ -19,9 +19,11 @@ import numpy as np
|
|
|
19
19
|
import pandas as pd
|
|
20
20
|
|
|
21
21
|
import mlrun
|
|
22
|
+
import mlrun.artifacts
|
|
22
23
|
import mlrun.common.model_monitoring.helpers
|
|
23
24
|
import mlrun.common.schemas.model_monitoring.constants as mm_constants
|
|
24
25
|
import mlrun.data_types.infer
|
|
26
|
+
import mlrun.model_monitoring
|
|
25
27
|
from mlrun.common.schemas.model_monitoring.model_endpoints import (
|
|
26
28
|
ModelEndpointMonitoringMetric,
|
|
27
29
|
ModelEndpointMonitoringMetricType,
|
|
@@ -253,7 +255,7 @@ def calculate_inputs_statistics(
|
|
|
253
255
|
)
|
|
254
256
|
|
|
255
257
|
# Recalculate the histograms over the bins that are set in the sample-set of the end point:
|
|
256
|
-
for feature in inputs_statistics
|
|
258
|
+
for feature in list(inputs_statistics):
|
|
257
259
|
if feature in sample_set_statistics:
|
|
258
260
|
counts, bins = np.histogram(
|
|
259
261
|
inputs[feature].to_numpy(),
|
|
@@ -270,6 +272,9 @@ def calculate_inputs_statistics(
|
|
|
270
272
|
inputs_statistics[feature]["hist"]
|
|
271
273
|
)
|
|
272
274
|
)
|
|
275
|
+
else:
|
|
276
|
+
# If the feature is not in the sample set and doesn't have a histogram, remove it from the statistics:
|
|
277
|
+
inputs_statistics.pop(feature)
|
|
273
278
|
|
|
274
279
|
return inputs_statistics
|
|
275
280
|
|
|
@@ -322,3 +327,35 @@ def get_invocations_metric(project: str) -> ModelEndpointMonitoringMetric:
|
|
|
322
327
|
name=mm_constants.PredictionsQueryConstants.INVOCATIONS,
|
|
323
328
|
full_name=get_invocations_fqn(project),
|
|
324
329
|
)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def enrich_model_endpoint_with_model_uri(
|
|
333
|
+
model_endpoint: ModelEndpoint,
|
|
334
|
+
model_obj: mlrun.artifacts.ModelArtifact,
|
|
335
|
+
):
|
|
336
|
+
"""
|
|
337
|
+
Enrich the model endpoint object with the model uri from the model object. We will use a unique reference
|
|
338
|
+
to the model object that includes the project, db_key, iter, and tree.
|
|
339
|
+
In addition, we verify that the model object is of type `ModelArtifact`.
|
|
340
|
+
|
|
341
|
+
:param model_endpoint: An object representing the model endpoint that will be enriched with the model uri.
|
|
342
|
+
:param model_obj: An object representing the model artifact.
|
|
343
|
+
|
|
344
|
+
:raise: `MLRunInvalidArgumentError` if the model object is not of type `ModelArtifact`.
|
|
345
|
+
"""
|
|
346
|
+
mlrun.utils.helpers.verify_field_of_type(
|
|
347
|
+
field_name="model_endpoint.spec.model_uri",
|
|
348
|
+
field_value=model_obj,
|
|
349
|
+
expected_type=mlrun.artifacts.ModelArtifact,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# Update model_uri with a unique reference to handle future changes
|
|
353
|
+
model_artifact_uri = mlrun.utils.helpers.generate_artifact_uri(
|
|
354
|
+
project=model_endpoint.metadata.project,
|
|
355
|
+
key=model_obj.db_key,
|
|
356
|
+
iter=model_obj.iter,
|
|
357
|
+
tree=model_obj.tree,
|
|
358
|
+
)
|
|
359
|
+
model_endpoint.spec.model_uri = mlrun.datastore.get_store_uri(
|
|
360
|
+
kind=mlrun.utils.helpers.StorePrefix.Model, uri=model_artifact_uri
|
|
361
|
+
)
|
|
@@ -37,6 +37,7 @@ from mlrun.common.schemas.model_monitoring.constants import (
|
|
|
37
37
|
ModelEndpointTarget,
|
|
38
38
|
ProjectSecretKeys,
|
|
39
39
|
)
|
|
40
|
+
from mlrun.model_monitoring.db import StoreBase, TSDBConnector
|
|
40
41
|
from mlrun.utils import logger
|
|
41
42
|
|
|
42
43
|
|
|
@@ -48,14 +49,12 @@ class EventStreamProcessor:
|
|
|
48
49
|
parquet_batching_max_events: int,
|
|
49
50
|
parquet_batching_timeout_secs: int,
|
|
50
51
|
parquet_target: str,
|
|
51
|
-
sample_window: int = 10,
|
|
52
52
|
aggregate_windows: typing.Optional[list[str]] = None,
|
|
53
|
-
aggregate_period: str = "
|
|
53
|
+
aggregate_period: str = "5m",
|
|
54
54
|
model_monitoring_access_key: str = None,
|
|
55
55
|
):
|
|
56
56
|
# General configurations, mainly used for the storey steps in the future serving graph
|
|
57
57
|
self.project = project
|
|
58
|
-
self.sample_window = sample_window
|
|
59
58
|
self.aggregate_windows = aggregate_windows or ["5m", "1h"]
|
|
60
59
|
self.aggregate_period = aggregate_period
|
|
61
60
|
|
|
@@ -133,7 +132,8 @@ class EventStreamProcessor:
|
|
|
133
132
|
def apply_monitoring_serving_graph(
|
|
134
133
|
self,
|
|
135
134
|
fn: mlrun.runtimes.ServingRuntime,
|
|
136
|
-
|
|
135
|
+
tsdb_connector: TSDBConnector,
|
|
136
|
+
endpoint_store: StoreBase,
|
|
137
137
|
) -> None:
|
|
138
138
|
"""
|
|
139
139
|
Apply monitoring serving graph to a given serving function. The following serving graph includes about 4 main
|
|
@@ -161,8 +161,8 @@ class EventStreamProcessor:
|
|
|
161
161
|
using CE, the parquet target path is based on the defined MLRun artifact path.
|
|
162
162
|
|
|
163
163
|
:param fn: A serving function.
|
|
164
|
-
:param
|
|
165
|
-
|
|
164
|
+
:param tsdb_connector: Time series database connector.
|
|
165
|
+
:param endpoint_store: KV/SQL store used for endpoint data.
|
|
166
166
|
"""
|
|
167
167
|
|
|
168
168
|
graph = typing.cast(
|
|
@@ -190,10 +190,6 @@ class EventStreamProcessor:
|
|
|
190
190
|
_fn="(event.get('error') is not None)",
|
|
191
191
|
)
|
|
192
192
|
|
|
193
|
-
tsdb_connector = mlrun.model_monitoring.get_tsdb_connector(
|
|
194
|
-
project=self.project, secret_provider=secret_provider
|
|
195
|
-
)
|
|
196
|
-
|
|
197
193
|
tsdb_connector.handle_model_error(
|
|
198
194
|
graph,
|
|
199
195
|
)
|
|
@@ -306,24 +302,9 @@ class EventStreamProcessor:
|
|
|
306
302
|
table=self.kv_path,
|
|
307
303
|
)
|
|
308
304
|
|
|
309
|
-
|
|
310
|
-
project=self.project, secret_provider=secret_provider
|
|
311
|
-
)
|
|
312
|
-
if store_object.type == ModelEndpointTarget.V3IO_NOSQL:
|
|
305
|
+
if endpoint_store.type == ModelEndpointTarget.V3IO_NOSQL:
|
|
313
306
|
apply_infer_schema()
|
|
314
307
|
|
|
315
|
-
# Emits the event in window size of events based on sample_window size (10 by default)
|
|
316
|
-
def apply_storey_sample_window():
|
|
317
|
-
graph.add_step(
|
|
318
|
-
"storey.steps.SampleWindow",
|
|
319
|
-
name="sample",
|
|
320
|
-
after="Rename",
|
|
321
|
-
window_size=self.sample_window,
|
|
322
|
-
key=EventFieldType.ENDPOINT_ID,
|
|
323
|
-
)
|
|
324
|
-
|
|
325
|
-
apply_storey_sample_window()
|
|
326
|
-
|
|
327
308
|
tsdb_connector.apply_monitoring_stream_steps(graph=graph)
|
|
328
309
|
|
|
329
310
|
# Parquet branch
|
|
@@ -353,6 +334,7 @@ class EventStreamProcessor:
|
|
|
353
334
|
index_cols=[EventFieldType.ENDPOINT_ID],
|
|
354
335
|
key_bucketing_number=0,
|
|
355
336
|
time_partitioning_granularity="hour",
|
|
337
|
+
time_field=EventFieldType.TIMESTAMP,
|
|
356
338
|
partition_cols=["$key", "$year", "$month", "$day", "$hour"],
|
|
357
339
|
)
|
|
358
340
|
|
mlrun/projects/project.py
CHANGED
|
@@ -1557,15 +1557,15 @@ class MlrunProject(ModelObj):
|
|
|
1557
1557
|
self,
|
|
1558
1558
|
item,
|
|
1559
1559
|
body=None,
|
|
1560
|
-
tag="",
|
|
1561
|
-
local_path="",
|
|
1562
|
-
artifact_path=None,
|
|
1563
|
-
format=None,
|
|
1564
|
-
upload=None,
|
|
1565
|
-
labels=None,
|
|
1566
|
-
target_path=None,
|
|
1560
|
+
tag: str = "",
|
|
1561
|
+
local_path: str = "",
|
|
1562
|
+
artifact_path: Optional[str] = None,
|
|
1563
|
+
format: Optional[str] = None,
|
|
1564
|
+
upload: Optional[bool] = None,
|
|
1565
|
+
labels: Optional[dict[str, str]] = None,
|
|
1566
|
+
target_path: Optional[str] = None,
|
|
1567
1567
|
**kwargs,
|
|
1568
|
-
):
|
|
1568
|
+
) -> Artifact:
|
|
1569
1569
|
"""Log an output artifact and optionally upload it to datastore
|
|
1570
1570
|
|
|
1571
1571
|
If the artifact already exists with the same key and tag, it will be overwritten.
|
|
@@ -1664,7 +1664,7 @@ class MlrunProject(ModelObj):
|
|
|
1664
1664
|
stats=None,
|
|
1665
1665
|
target_path="",
|
|
1666
1666
|
extra_data=None,
|
|
1667
|
-
label_column: str = None,
|
|
1667
|
+
label_column: Optional[str] = None,
|
|
1668
1668
|
**kwargs,
|
|
1669
1669
|
) -> DatasetArtifact:
|
|
1670
1670
|
"""
|
|
@@ -1741,15 +1741,15 @@ class MlrunProject(ModelObj):
|
|
|
1741
1741
|
artifact_path=None,
|
|
1742
1742
|
upload=None,
|
|
1743
1743
|
labels=None,
|
|
1744
|
-
inputs: list[Feature] = None,
|
|
1745
|
-
outputs: list[Feature] = None,
|
|
1746
|
-
feature_vector: str = None,
|
|
1747
|
-
feature_weights: list = None,
|
|
1744
|
+
inputs: Optional[list[Feature]] = None,
|
|
1745
|
+
outputs: Optional[list[Feature]] = None,
|
|
1746
|
+
feature_vector: Optional[str] = None,
|
|
1747
|
+
feature_weights: Optional[list] = None,
|
|
1748
1748
|
training_set=None,
|
|
1749
1749
|
label_column=None,
|
|
1750
1750
|
extra_data=None,
|
|
1751
1751
|
**kwargs,
|
|
1752
|
-
):
|
|
1752
|
+
) -> ModelArtifact:
|
|
1753
1753
|
"""Log a model artifact and optionally upload it to datastore
|
|
1754
1754
|
|
|
1755
1755
|
If the model already exists with the same key and tag, it will be overwritten.
|
|
@@ -3040,8 +3040,9 @@ class MlrunProject(ModelObj):
|
|
|
3040
3040
|
"Remote repo is not defined, use .create_remote() + push()"
|
|
3041
3041
|
)
|
|
3042
3042
|
|
|
3043
|
-
if engine not in ["remote"]:
|
|
3044
|
-
#
|
|
3043
|
+
if engine not in ["remote"] and not schedule:
|
|
3044
|
+
# For remote/scheduled runs we don't require the functions to be synced as they can be loaded dynamically
|
|
3045
|
+
# during run
|
|
3045
3046
|
self.sync_functions(always=sync)
|
|
3046
3047
|
if not self.spec._function_objects:
|
|
3047
3048
|
raise ValueError(
|
|
@@ -578,6 +578,15 @@ class APIGateway(ModelObj):
|
|
|
578
578
|
"true"
|
|
579
579
|
)
|
|
580
580
|
|
|
581
|
+
def with_gateway_timeout(self, gateway_timeout: int):
|
|
582
|
+
"""
|
|
583
|
+
Set gateway proxy connect/read/send timeout annotations
|
|
584
|
+
:param gateway_timeout: The timeout in seconds
|
|
585
|
+
"""
|
|
586
|
+
mlrun.runtimes.utils.enrich_gateway_timeout_annotations(
|
|
587
|
+
self.metadata.annotations, gateway_timeout
|
|
588
|
+
)
|
|
589
|
+
|
|
581
590
|
@classmethod
|
|
582
591
|
def from_scheme(cls, api_gateway: schemas.APIGateway):
|
|
583
592
|
project = api_gateway.metadata.labels.get(
|
|
@@ -15,6 +15,7 @@ import pathlib
|
|
|
15
15
|
import typing
|
|
16
16
|
|
|
17
17
|
import nuclio
|
|
18
|
+
import nuclio.auth
|
|
18
19
|
|
|
19
20
|
import mlrun.common.schemas as schemas
|
|
20
21
|
import mlrun.errors
|
|
@@ -281,34 +282,29 @@ class ApplicationRuntime(RemoteRuntime):
|
|
|
281
282
|
is_kfp=False,
|
|
282
283
|
mlrun_version_specifier=None,
|
|
283
284
|
show_on_failure: bool = False,
|
|
284
|
-
|
|
285
|
-
authentication_mode: schemas.APIGatewayAuthenticationMode = None,
|
|
286
|
-
authentication_creds: tuple[str] = None,
|
|
287
|
-
ssl_redirect: bool = None,
|
|
285
|
+
create_default_api_gateway: bool = True,
|
|
288
286
|
):
|
|
289
287
|
"""
|
|
290
288
|
Deploy function, builds the application image if required (self.requires_build()) or force_build is True,
|
|
291
289
|
Once the image is built, the function is deployed.
|
|
292
290
|
|
|
293
|
-
:param project:
|
|
294
|
-
:param tag:
|
|
295
|
-
:param verbose:
|
|
296
|
-
:param auth_info:
|
|
297
|
-
:param builder_env:
|
|
298
|
-
|
|
299
|
-
:param force_build:
|
|
300
|
-
:param with_mlrun:
|
|
301
|
-
:param skip_deployed:
|
|
302
|
-
:param is_kfp:
|
|
303
|
-
:param mlrun_version_specifier:
|
|
304
|
-
:param show_on_failure:
|
|
305
|
-
:param
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
:return: True if the function is ready (deployed)
|
|
291
|
+
:param project: Project name
|
|
292
|
+
:param tag: Function tag
|
|
293
|
+
:param verbose: Set True for verbose logging
|
|
294
|
+
:param auth_info: Service AuthInfo (deprecated and ignored)
|
|
295
|
+
:param builder_env: Env vars dict for source archive config/credentials
|
|
296
|
+
e.g. builder_env={"GIT_TOKEN": token}
|
|
297
|
+
:param force_build: Set True for force building the application image
|
|
298
|
+
:param with_mlrun: Add the current mlrun package to the container build
|
|
299
|
+
:param skip_deployed: Skip the build if we already have an image for the function
|
|
300
|
+
:param is_kfp: Deploy as part of a kfp pipeline
|
|
301
|
+
:param mlrun_version_specifier: Which mlrun package version to include (if not current)
|
|
302
|
+
:param show_on_failure: Show logs only in case of build failure
|
|
303
|
+
:param create_default_api_gateway: When deploy finishes the default API gateway will be created for the
|
|
304
|
+
application. Disabling this flag means that the application will not be
|
|
305
|
+
accessible until an API gateway is created for it.
|
|
306
|
+
|
|
307
|
+
:return: The default API gateway URL if created or True if the function is ready (deployed)
|
|
312
308
|
"""
|
|
313
309
|
if (self.requires_build() and not self.spec.image) or force_build:
|
|
314
310
|
self._fill_credentials()
|
|
@@ -328,10 +324,6 @@ class ApplicationRuntime(RemoteRuntime):
|
|
|
328
324
|
self._configure_application_sidecar()
|
|
329
325
|
|
|
330
326
|
# We only allow accessing the application via the API Gateway
|
|
331
|
-
name_tag = tag or self.metadata.tag
|
|
332
|
-
self.status.api_gateway_name = (
|
|
333
|
-
f"{self.metadata.name}-{name_tag}" if name_tag else self.metadata.name
|
|
334
|
-
)
|
|
335
327
|
self.spec.add_templated_ingress_host_mode = (
|
|
336
328
|
NuclioIngressAddTemplatedIngressModes.never
|
|
337
329
|
)
|
|
@@ -344,9 +336,7 @@ class ApplicationRuntime(RemoteRuntime):
|
|
|
344
336
|
builder_env=builder_env,
|
|
345
337
|
)
|
|
346
338
|
logger.info(
|
|
347
|
-
"Successfully deployed function
|
|
348
|
-
api_gateway_name=self.status.api_gateway_name,
|
|
349
|
-
authentication_mode=authentication_mode,
|
|
339
|
+
"Successfully deployed function.",
|
|
350
340
|
)
|
|
351
341
|
|
|
352
342
|
# Restore the source in case it was removed to make nuclio not consider it when building
|
|
@@ -354,14 +344,23 @@ class ApplicationRuntime(RemoteRuntime):
|
|
|
354
344
|
self.spec.build.source = self.status.application_source
|
|
355
345
|
self.save(versioned=False)
|
|
356
346
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
347
|
+
if create_default_api_gateway:
|
|
348
|
+
try:
|
|
349
|
+
api_gateway_name = self.resolve_default_api_gateway_name()
|
|
350
|
+
return self.create_api_gateway(api_gateway_name, set_as_default=True)
|
|
351
|
+
except Exception as exc:
|
|
352
|
+
logger.warning(
|
|
353
|
+
"Failed to create default API gateway, application may not be accessible. "
|
|
354
|
+
"Use the `create_api_gateway` method to make it accessible",
|
|
355
|
+
exc=mlrun.errors.err_to_str(exc),
|
|
356
|
+
)
|
|
357
|
+
elif not self.status.api_gateway:
|
|
358
|
+
logger.warning(
|
|
359
|
+
"Application is online but may not be accessible since default gateway creation was not requested."
|
|
360
|
+
"Use the `create_api_gateway` method to make it accessible."
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
return True
|
|
365
364
|
|
|
366
365
|
def with_source_archive(
|
|
367
366
|
self,
|
|
@@ -429,17 +428,54 @@ class ApplicationRuntime(RemoteRuntime):
|
|
|
429
428
|
self,
|
|
430
429
|
name: str = None,
|
|
431
430
|
path: str = None,
|
|
432
|
-
|
|
431
|
+
direct_port_access: bool = False,
|
|
433
432
|
authentication_mode: schemas.APIGatewayAuthenticationMode = None,
|
|
434
|
-
authentication_creds: tuple[str] = None,
|
|
433
|
+
authentication_creds: tuple[str, str] = None,
|
|
435
434
|
ssl_redirect: bool = None,
|
|
435
|
+
set_as_default: bool = False,
|
|
436
|
+
gateway_timeout: typing.Optional[int] = None,
|
|
436
437
|
):
|
|
438
|
+
"""
|
|
439
|
+
Create the application API gateway. Once the application is deployed, the API gateway can be created.
|
|
440
|
+
An application without an API gateway is not accessible.
|
|
441
|
+
:param name: The name of the API gateway, defaults to <function-name>-<function-tag>
|
|
442
|
+
:param path: Optional path of the API gateway, default value is "/"
|
|
443
|
+
:param direct_port_access: Set True to allow direct port access to the application sidecar
|
|
444
|
+
:param authentication_mode: API Gateway authentication mode
|
|
445
|
+
:param authentication_creds: API Gateway basic authentication credentials as a tuple (username, password)
|
|
446
|
+
:param ssl_redirect: Set True to force SSL redirect, False to disable. Defaults to
|
|
447
|
+
mlrun.mlconf.force_api_gateway_ssl_redirect()
|
|
448
|
+
:param set_as_default: Set the API gateway as the default for the application (`status.api_gateway`)
|
|
449
|
+
:param gateway_timeout: nginx ingress timeout in sec (request timeout, when will the gateway return an
|
|
450
|
+
error)
|
|
451
|
+
|
|
452
|
+
:return: The API gateway URL
|
|
453
|
+
"""
|
|
454
|
+
if not name:
|
|
455
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
456
|
+
"API gateway name must be specified."
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
if not set_as_default and name == self.resolve_default_api_gateway_name():
|
|
460
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
461
|
+
f"Non-default API gateway cannot use the default gateway name, {name=}."
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
if (
|
|
465
|
+
authentication_mode == schemas.APIGatewayAuthenticationMode.basic
|
|
466
|
+
and not authentication_creds
|
|
467
|
+
):
|
|
468
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
469
|
+
"Authentication credentials not provided"
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
ports = self.spec.internal_application_port if direct_port_access else []
|
|
473
|
+
|
|
437
474
|
api_gateway = APIGateway(
|
|
438
475
|
APIGatewayMetadata(
|
|
439
476
|
name=name,
|
|
440
477
|
namespace=self.metadata.namespace,
|
|
441
|
-
labels=self.metadata.labels,
|
|
442
|
-
annotations=self.metadata.annotations,
|
|
478
|
+
labels=self.metadata.labels.copy(),
|
|
443
479
|
),
|
|
444
480
|
APIGatewaySpec(
|
|
445
481
|
functions=[self],
|
|
@@ -449,13 +485,14 @@ class ApplicationRuntime(RemoteRuntime):
|
|
|
449
485
|
),
|
|
450
486
|
)
|
|
451
487
|
|
|
488
|
+
api_gateway.with_gateway_timeout(gateway_timeout)
|
|
452
489
|
if ssl_redirect is None:
|
|
453
490
|
ssl_redirect = mlrun.mlconf.force_api_gateway_ssl_redirect()
|
|
454
491
|
if ssl_redirect:
|
|
455
|
-
#
|
|
492
|
+
# Force ssl redirect so that the application is only accessible via https
|
|
456
493
|
api_gateway.with_force_ssl_redirect()
|
|
457
494
|
|
|
458
|
-
#
|
|
495
|
+
# Add authentication if required
|
|
459
496
|
authentication_mode = (
|
|
460
497
|
authentication_mode
|
|
461
498
|
or mlrun.mlconf.function.application.default_authentication_mode
|
|
@@ -469,18 +506,38 @@ class ApplicationRuntime(RemoteRuntime):
|
|
|
469
506
|
api_gateway_scheme = db.store_api_gateway(
|
|
470
507
|
api_gateway=api_gateway.to_scheme(), project=self.metadata.project
|
|
471
508
|
)
|
|
472
|
-
if not self.status.api_gateway_name:
|
|
473
|
-
self.status.api_gateway_name = api_gateway_scheme.metadata.name
|
|
474
|
-
self.status.api_gateway = APIGateway.from_scheme(api_gateway_scheme)
|
|
475
|
-
self.status.api_gateway.wait_for_readiness()
|
|
476
|
-
self.url = self.status.api_gateway.invoke_url
|
|
477
509
|
|
|
478
|
-
|
|
479
|
-
|
|
510
|
+
if set_as_default:
|
|
511
|
+
self.status.api_gateway_name = api_gateway_scheme.metadata.name
|
|
512
|
+
self.status.api_gateway = APIGateway.from_scheme(api_gateway_scheme)
|
|
513
|
+
self.status.api_gateway.wait_for_readiness()
|
|
514
|
+
self.url = self.status.api_gateway.invoke_url
|
|
515
|
+
url = self.url
|
|
516
|
+
else:
|
|
517
|
+
api_gateway = APIGateway.from_scheme(api_gateway_scheme)
|
|
518
|
+
api_gateway.wait_for_readiness()
|
|
519
|
+
url = api_gateway.invoke_url
|
|
520
|
+
# Update application status (enriches invocation url)
|
|
521
|
+
self._get_state(raise_on_exception=False)
|
|
522
|
+
|
|
523
|
+
logger.info("Successfully created API gateway", url=url)
|
|
524
|
+
return url
|
|
525
|
+
|
|
526
|
+
def delete_api_gateway(self, name: str):
|
|
527
|
+
"""
|
|
528
|
+
Delete API gateway by name.
|
|
529
|
+
Refreshes the application status to update api gateway and invocation URLs.
|
|
530
|
+
:param name: The API gateway name
|
|
531
|
+
"""
|
|
532
|
+
self._get_db().delete_api_gateway(name=name, project=self.metadata.project)
|
|
533
|
+
if name == self.status.api_gateway_name:
|
|
534
|
+
self.status.api_gateway_name = None
|
|
535
|
+
self.status.api_gateway = None
|
|
536
|
+
self._get_state()
|
|
480
537
|
|
|
481
538
|
def invoke(
|
|
482
539
|
self,
|
|
483
|
-
path: str,
|
|
540
|
+
path: str = "",
|
|
484
541
|
body: typing.Optional[typing.Union[str, bytes, dict]] = None,
|
|
485
542
|
method: str = None,
|
|
486
543
|
headers: dict = None,
|
|
@@ -488,12 +545,25 @@ class ApplicationRuntime(RemoteRuntime):
|
|
|
488
545
|
force_external_address: bool = False,
|
|
489
546
|
auth_info: schemas.AuthInfo = None,
|
|
490
547
|
mock: bool = None,
|
|
548
|
+
credentials: tuple[str, str] = None,
|
|
491
549
|
**http_client_kwargs,
|
|
492
550
|
):
|
|
493
551
|
self._sync_api_gateway()
|
|
552
|
+
|
|
494
553
|
# If the API Gateway is not ready or not set, try to invoke the function directly (without the API Gateway)
|
|
495
554
|
if not self.status.api_gateway:
|
|
496
|
-
|
|
555
|
+
logger.warning(
|
|
556
|
+
"Default API gateway is not configured, invoking function invocation URL."
|
|
557
|
+
)
|
|
558
|
+
# create a requests auth object if credentials are provided and not already set in the http client kwargs
|
|
559
|
+
auth = http_client_kwargs.pop("auth", None) or (
|
|
560
|
+
nuclio.auth.AuthInfo(
|
|
561
|
+
username=credentials[0], password=credentials[1]
|
|
562
|
+
).to_requests_auth()
|
|
563
|
+
if credentials
|
|
564
|
+
else None
|
|
565
|
+
)
|
|
566
|
+
return super().invoke(
|
|
497
567
|
path,
|
|
498
568
|
body,
|
|
499
569
|
method,
|
|
@@ -502,11 +572,10 @@ class ApplicationRuntime(RemoteRuntime):
|
|
|
502
572
|
force_external_address,
|
|
503
573
|
auth_info,
|
|
504
574
|
mock,
|
|
575
|
+
auth=auth,
|
|
505
576
|
**http_client_kwargs,
|
|
506
577
|
)
|
|
507
578
|
|
|
508
|
-
credentials = (auth_info.username, auth_info.password) if auth_info else None
|
|
509
|
-
|
|
510
579
|
if not method:
|
|
511
580
|
method = "POST" if body else "GET"
|
|
512
581
|
|
|
@@ -552,6 +621,13 @@ class ApplicationRuntime(RemoteRuntime):
|
|
|
552
621
|
reverse_proxy_func.metadata.name, reverse_proxy_func.metadata.project
|
|
553
622
|
)
|
|
554
623
|
|
|
624
|
+
def resolve_default_api_gateway_name(self):
|
|
625
|
+
return (
|
|
626
|
+
f"{self.metadata.name}-{self.metadata.tag}"
|
|
627
|
+
if self.metadata.tag
|
|
628
|
+
else self.metadata.name
|
|
629
|
+
)
|
|
630
|
+
|
|
555
631
|
@min_nuclio_versions("1.13.1")
|
|
556
632
|
def disable_default_http_trigger(
|
|
557
633
|
self,
|