mlrun 1.7.0rc6__py3-none-any.whl → 1.7.0rc9__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/__main__.py +2 -0
- mlrun/common/constants.py +6 -0
- mlrun/common/schemas/__init__.py +5 -0
- mlrun/common/schemas/api_gateway.py +8 -1
- mlrun/common/schemas/hub.py +7 -9
- mlrun/common/schemas/model_monitoring/__init__.py +4 -0
- mlrun/common/schemas/model_monitoring/constants.py +36 -19
- mlrun/{model_monitoring/stores/models/__init__.py → common/schemas/pagination.py} +9 -10
- mlrun/common/schemas/project.py +16 -10
- mlrun/common/types.py +7 -1
- mlrun/config.py +35 -10
- mlrun/data_types/data_types.py +4 -0
- mlrun/datastore/__init__.py +3 -7
- 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/datastore_profile.py +19 -1
- mlrun/datastore/google_cloud_storage.py +1 -1
- mlrun/datastore/snowflake_utils.py +43 -0
- mlrun/datastore/sources.py +11 -29
- mlrun/datastore/targets.py +131 -11
- mlrun/datastore/utils.py +10 -5
- mlrun/db/base.py +58 -6
- mlrun/db/httpdb.py +183 -77
- mlrun/db/nopdb.py +110 -0
- mlrun/feature_store/api.py +3 -2
- mlrun/feature_store/retrieval/spark_merger.py +27 -23
- mlrun/frameworks/tf_keras/callbacks/logging_callback.py +1 -1
- mlrun/frameworks/tf_keras/mlrun_interface.py +2 -2
- mlrun/kfpops.py +2 -5
- mlrun/launcher/base.py +1 -1
- mlrun/launcher/client.py +2 -2
- mlrun/model.py +1 -0
- mlrun/model_monitoring/__init__.py +1 -1
- mlrun/model_monitoring/api.py +104 -295
- mlrun/model_monitoring/controller.py +25 -25
- 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/helpers.py +3 -3
- mlrun/model_monitoring/stream_processing.py +41 -9
- mlrun/model_monitoring/tracking_policy.py +7 -1
- mlrun/model_monitoring/writer.py +4 -36
- mlrun/projects/pipelines.py +14 -2
- mlrun/projects/project.py +141 -122
- mlrun/run.py +8 -2
- mlrun/runtimes/__init__.py +16 -0
- mlrun/runtimes/base.py +10 -1
- mlrun/runtimes/kubejob.py +26 -121
- mlrun/runtimes/nuclio/api_gateway.py +243 -66
- mlrun/runtimes/nuclio/application/application.py +79 -1
- mlrun/runtimes/nuclio/application/reverse_proxy.go +9 -1
- mlrun/runtimes/nuclio/function.py +14 -8
- mlrun/runtimes/nuclio/serving.py +30 -34
- mlrun/runtimes/pod.py +171 -0
- mlrun/runtimes/utils.py +0 -28
- mlrun/serving/remote.py +2 -3
- mlrun/serving/routers.py +4 -3
- mlrun/serving/server.py +5 -7
- mlrun/serving/states.py +40 -23
- mlrun/serving/v2_serving.py +4 -3
- mlrun/utils/helpers.py +34 -0
- mlrun/utils/http.py +1 -1
- mlrun/utils/retryer.py +1 -0
- mlrun/utils/version/version.json +2 -2
- {mlrun-1.7.0rc6.dist-info → mlrun-1.7.0rc9.dist-info}/METADATA +25 -16
- {mlrun-1.7.0rc6.dist-info → mlrun-1.7.0rc9.dist-info}/RECORD +81 -75
- mlrun/model_monitoring/batch.py +0 -933
- mlrun/model_monitoring/stores/models/mysql.py +0 -34
- mlrun/model_monitoring/stores/sql_model_endpoint_store.py +0 -382
- {mlrun-1.7.0rc6.dist-info → mlrun-1.7.0rc9.dist-info}/LICENSE +0 -0
- {mlrun-1.7.0rc6.dist-info → mlrun-1.7.0rc9.dist-info}/WHEEL +0 -0
- {mlrun-1.7.0rc6.dist-info → mlrun-1.7.0rc9.dist-info}/entry_points.txt +0 -0
- {mlrun-1.7.0rc6.dist-info → mlrun-1.7.0rc9.dist-info}/top_level.txt +0 -0
mlrun/runtimes/kubejob.py
CHANGED
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
-
import time
|
|
16
15
|
import warnings
|
|
17
16
|
|
|
18
17
|
import mlrun.common.schemas
|
|
@@ -21,7 +20,6 @@ import mlrun.errors
|
|
|
21
20
|
|
|
22
21
|
from ..kfpops import build_op
|
|
23
22
|
from ..model import RunObject
|
|
24
|
-
from ..utils import get_in, logger
|
|
25
23
|
from .pod import KubeResource
|
|
26
24
|
|
|
27
25
|
|
|
@@ -65,29 +63,13 @@ class KubejobRuntime(KubeResource):
|
|
|
65
63
|
:param pull_at_runtime: load the archive into the container at job runtime vs on build/deploy
|
|
66
64
|
:param target_dir: target dir on runtime pod or repo clone / archive extraction
|
|
67
65
|
"""
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if target_dir:
|
|
76
|
-
self.spec.build.source_code_target_dir = target_dir
|
|
77
|
-
|
|
78
|
-
self.spec.build.load_source_on_run = pull_at_runtime
|
|
79
|
-
if (
|
|
80
|
-
self.spec.build.base_image
|
|
81
|
-
and not self.spec.build.commands
|
|
82
|
-
and pull_at_runtime
|
|
83
|
-
and not self.spec.image
|
|
84
|
-
):
|
|
85
|
-
# if we load source from repo and don't need a full build use the base_image as the image
|
|
86
|
-
self.spec.image = self.spec.build.base_image
|
|
87
|
-
elif not pull_at_runtime:
|
|
88
|
-
# clear the image so build will not be skipped
|
|
89
|
-
self.spec.build.base_image = self.spec.build.base_image or self.spec.image
|
|
90
|
-
self.spec.image = ""
|
|
66
|
+
self._configure_mlrun_build_with_source(
|
|
67
|
+
source=source,
|
|
68
|
+
workdir=workdir,
|
|
69
|
+
handler=handler,
|
|
70
|
+
pull_at_runtime=pull_at_runtime,
|
|
71
|
+
target_dir=target_dir,
|
|
72
|
+
)
|
|
91
73
|
|
|
92
74
|
def build_config(
|
|
93
75
|
self,
|
|
@@ -169,116 +151,39 @@ class KubejobRuntime(KubeResource):
|
|
|
169
151
|
show_on_failure: bool = False,
|
|
170
152
|
force_build: bool = False,
|
|
171
153
|
) -> bool:
|
|
172
|
-
"""
|
|
154
|
+
"""Deploy function, build container with dependencies
|
|
173
155
|
|
|
174
|
-
:param watch:
|
|
175
|
-
:param with_mlrun:
|
|
176
|
-
:param skip_deployed:
|
|
177
|
-
:param is_kfp:
|
|
178
|
-
:param mlrun_version_specifier:
|
|
156
|
+
:param watch: Wait for the deploy to complete (and print build logs)
|
|
157
|
+
:param with_mlrun: Add the current mlrun package to the container build
|
|
158
|
+
:param skip_deployed: Skip the build if we already have an image for the function
|
|
159
|
+
:param is_kfp: Deploy as part of a kfp pipeline
|
|
160
|
+
:param mlrun_version_specifier: Which mlrun package version to include (if not current)
|
|
179
161
|
:param builder_env: Kaniko builder pod env vars dict (for config/credentials)
|
|
180
162
|
e.g. builder_env={"GIT_TOKEN": token}
|
|
181
|
-
:param show_on_failure:
|
|
182
|
-
:param force_build:
|
|
163
|
+
:param show_on_failure: Show logs only in case of build failure
|
|
164
|
+
:param force_build: Set True for force building the image, even when no changes were made
|
|
183
165
|
|
|
184
166
|
:return: True if the function is ready (deployed)
|
|
185
167
|
"""
|
|
186
168
|
|
|
187
169
|
build = self.spec.build
|
|
170
|
+
with_mlrun = self._resolve_build_with_mlrun(with_mlrun)
|
|
188
171
|
|
|
189
|
-
if with_mlrun is None:
|
|
190
|
-
if build.with_mlrun is not None:
|
|
191
|
-
with_mlrun = build.with_mlrun
|
|
192
|
-
else:
|
|
193
|
-
with_mlrun = build.base_image and not (
|
|
194
|
-
build.base_image.startswith("mlrun/")
|
|
195
|
-
or "/mlrun/" in build.base_image
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
if (
|
|
199
|
-
not build.source
|
|
200
|
-
and not build.commands
|
|
201
|
-
and not build.requirements
|
|
202
|
-
and not build.extra
|
|
203
|
-
and with_mlrun
|
|
204
|
-
):
|
|
205
|
-
logger.info(
|
|
206
|
-
"Running build to add mlrun package, set "
|
|
207
|
-
"with_mlrun=False to skip if its already in the image"
|
|
208
|
-
)
|
|
209
172
|
self.status.state = ""
|
|
210
173
|
if build.base_image:
|
|
211
174
|
# clear the image so build will not be skipped
|
|
212
175
|
self.spec.image = ""
|
|
213
176
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
with_mlrun,
|
|
225
|
-
mlrun_version_specifier,
|
|
226
|
-
skip_deployed,
|
|
227
|
-
builder_env=builder_env,
|
|
228
|
-
force_build=force_build,
|
|
229
|
-
)
|
|
230
|
-
self.status = data["data"].get("status", None)
|
|
231
|
-
self.spec.image = get_in(data, "data.spec.image")
|
|
232
|
-
self.spec.build.base_image = self.spec.build.base_image or get_in(
|
|
233
|
-
data, "data.spec.build.base_image"
|
|
234
|
-
)
|
|
235
|
-
# Get the source target dir in case it was enriched due to loading source
|
|
236
|
-
self.spec.build.source_code_target_dir = get_in(
|
|
237
|
-
data, "data.spec.build.source_code_target_dir"
|
|
238
|
-
) or get_in(data, "data.spec.clone_target_dir")
|
|
239
|
-
ready = data.get("ready", False)
|
|
240
|
-
if not ready:
|
|
241
|
-
logger.info(
|
|
242
|
-
f"Started building image: {data.get('data', {}).get('spec', {}).get('build', {}).get('image')}"
|
|
243
|
-
)
|
|
244
|
-
if watch and not ready:
|
|
245
|
-
state = self._build_watch(watch, show_on_failure=show_on_failure)
|
|
246
|
-
ready = state == "ready"
|
|
247
|
-
self.status.state = state
|
|
248
|
-
|
|
249
|
-
if watch and not ready:
|
|
250
|
-
raise mlrun.errors.MLRunRuntimeError("Deploy failed")
|
|
251
|
-
return ready
|
|
252
|
-
|
|
253
|
-
def _build_watch(self, watch=True, logs=True, show_on_failure=False):
|
|
254
|
-
db = self._get_db()
|
|
255
|
-
offset = 0
|
|
256
|
-
try:
|
|
257
|
-
text, _ = db.get_builder_status(self, 0, logs=logs)
|
|
258
|
-
except mlrun.db.RunDBError:
|
|
259
|
-
raise ValueError("function or build process not found")
|
|
260
|
-
|
|
261
|
-
def print_log(text):
|
|
262
|
-
if text and (not show_on_failure or self.status.state == "error"):
|
|
263
|
-
print(text, end="")
|
|
264
|
-
|
|
265
|
-
print_log(text)
|
|
266
|
-
offset += len(text)
|
|
267
|
-
if watch:
|
|
268
|
-
while self.status.state in ["pending", "running"]:
|
|
269
|
-
time.sleep(2)
|
|
270
|
-
if show_on_failure:
|
|
271
|
-
text = ""
|
|
272
|
-
db.get_builder_status(self, 0, logs=False)
|
|
273
|
-
if self.status.state == "error":
|
|
274
|
-
# re-read the full log on failure
|
|
275
|
-
text, _ = db.get_builder_status(self, offset, logs=logs)
|
|
276
|
-
else:
|
|
277
|
-
text, _ = db.get_builder_status(self, offset, logs=logs)
|
|
278
|
-
print_log(text)
|
|
279
|
-
offset += len(text)
|
|
280
|
-
|
|
281
|
-
return self.status.state
|
|
177
|
+
return self._build_image(
|
|
178
|
+
builder_env=builder_env,
|
|
179
|
+
force_build=force_build,
|
|
180
|
+
mlrun_version_specifier=mlrun_version_specifier,
|
|
181
|
+
show_on_failure=show_on_failure,
|
|
182
|
+
skip_deployed=skip_deployed,
|
|
183
|
+
watch=watch,
|
|
184
|
+
is_kfp=is_kfp,
|
|
185
|
+
with_mlrun=with_mlrun,
|
|
186
|
+
)
|
|
282
187
|
|
|
283
188
|
def deploy_step(
|
|
284
189
|
self,
|
|
@@ -12,15 +12,18 @@
|
|
|
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
|
|
25
|
+
from ..utils import logger
|
|
26
|
+
from .function import RemoteRuntime, get_fullname, min_nuclio_versions
|
|
24
27
|
from .serving import ServingRuntime
|
|
25
28
|
|
|
26
29
|
NUCLIO_API_GATEWAY_AUTHENTICATION_MODE_BASIC_AUTH = "basicAuth"
|
|
@@ -28,7 +31,69 @@ NUCLIO_API_GATEWAY_AUTHENTICATION_MODE_NONE = "none"
|
|
|
28
31
|
PROJECT_NAME_LABEL = "nuclio.io/project-name"
|
|
29
32
|
|
|
30
33
|
|
|
34
|
+
class APIGatewayAuthenticator(typing.Protocol):
|
|
35
|
+
@property
|
|
36
|
+
def authentication_mode(self) -> str:
|
|
37
|
+
return NUCLIO_API_GATEWAY_AUTHENTICATION_MODE_NONE
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def from_scheme(cls, api_gateway_spec: mlrun.common.schemas.APIGatewaySpec):
|
|
41
|
+
if (
|
|
42
|
+
api_gateway_spec.authenticationMode
|
|
43
|
+
== NUCLIO_API_GATEWAY_AUTHENTICATION_MODE_BASIC_AUTH
|
|
44
|
+
):
|
|
45
|
+
if api_gateway_spec.authentication:
|
|
46
|
+
return BasicAuth(
|
|
47
|
+
username=api_gateway_spec.authentication.get("username", ""),
|
|
48
|
+
password=api_gateway_spec.authentication.get("password", ""),
|
|
49
|
+
)
|
|
50
|
+
else:
|
|
51
|
+
return BasicAuth()
|
|
52
|
+
else:
|
|
53
|
+
return NoneAuth()
|
|
54
|
+
|
|
55
|
+
def to_scheme(
|
|
56
|
+
self,
|
|
57
|
+
) -> Optional[dict[str, Optional[mlrun.common.schemas.APIGatewayBasicAuth]]]:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class NoneAuth(APIGatewayAuthenticator):
|
|
62
|
+
"""
|
|
63
|
+
An API gateway authenticator with no authentication.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class BasicAuth(APIGatewayAuthenticator):
|
|
70
|
+
"""
|
|
71
|
+
An API gateway authenticator with basic authentication.
|
|
72
|
+
|
|
73
|
+
:param username: (str) The username for basic authentication.
|
|
74
|
+
:param password: (str) The password for basic authentication.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(self, username=None, password=None):
|
|
78
|
+
self._username = username
|
|
79
|
+
self._password = password
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def authentication_mode(self) -> str:
|
|
83
|
+
return NUCLIO_API_GATEWAY_AUTHENTICATION_MODE_BASIC_AUTH
|
|
84
|
+
|
|
85
|
+
def to_scheme(
|
|
86
|
+
self,
|
|
87
|
+
) -> Optional[dict[str, Optional[mlrun.common.schemas.APIGatewayBasicAuth]]]:
|
|
88
|
+
return {
|
|
89
|
+
"basicAuth": mlrun.common.schemas.APIGatewayBasicAuth(
|
|
90
|
+
username=self._username, password=self._password
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
31
95
|
class APIGateway:
|
|
96
|
+
@min_nuclio_versions("1.13.1")
|
|
32
97
|
def __init__(
|
|
33
98
|
self,
|
|
34
99
|
project,
|
|
@@ -47,22 +112,34 @@ class APIGateway:
|
|
|
47
112
|
],
|
|
48
113
|
description: str = "",
|
|
49
114
|
path: str = "/",
|
|
50
|
-
|
|
51
|
-
str
|
|
52
|
-
] = NUCLIO_API_GATEWAY_AUTHENTICATION_MODE_NONE,
|
|
115
|
+
authentication: Optional[APIGatewayAuthenticator] = NoneAuth(),
|
|
53
116
|
host: Optional[str] = None,
|
|
54
117
|
canary: Optional[list[int]] = None,
|
|
55
|
-
username: Optional[str] = None,
|
|
56
|
-
password: Optional[str] = None,
|
|
57
118
|
):
|
|
119
|
+
"""
|
|
120
|
+
Initialize the APIGateway instance.
|
|
121
|
+
|
|
122
|
+
:param project: The project name
|
|
123
|
+
:param name: The name of the API gateway
|
|
124
|
+
:param functions: The list of functions associated with the API gateway
|
|
125
|
+
Can be a list of function names (["my-func1", "my-func2"])
|
|
126
|
+
or a list or a single entity of
|
|
127
|
+
:py:class:`~mlrun.runtimes.nuclio.function.RemoteRuntime` OR
|
|
128
|
+
:py:class:`~mlrun.runtimes.nuclio.serving.ServingRuntime`
|
|
129
|
+
|
|
130
|
+
:param description: Optional description of the API gateway
|
|
131
|
+
:param path: Optional path of the API gateway, default value is "/"
|
|
132
|
+
:param authentication: The authentication for the API gateway of type
|
|
133
|
+
:py:class:`~mlrun.runtimes.nuclio.api_gateway.BasicAuth`
|
|
134
|
+
:param host: The host of the API gateway (optional). If not set, it will be automatically generated
|
|
135
|
+
:param canary: The canary percents for the API gateway of type list[int]; for instance: [20,80]
|
|
136
|
+
"""
|
|
58
137
|
self.functions = None
|
|
59
138
|
self._validate(
|
|
60
139
|
project=project,
|
|
61
140
|
functions=functions,
|
|
62
141
|
name=name,
|
|
63
142
|
canary=canary,
|
|
64
|
-
username=username,
|
|
65
|
-
password=password,
|
|
66
143
|
)
|
|
67
144
|
self.project = project
|
|
68
145
|
self.name = name
|
|
@@ -70,14 +147,9 @@ class APIGateway:
|
|
|
70
147
|
|
|
71
148
|
self.path = path
|
|
72
149
|
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
150
|
self.canary = canary
|
|
79
|
-
self.
|
|
80
|
-
self.
|
|
151
|
+
self.authentication = authentication
|
|
152
|
+
self.state = ""
|
|
81
153
|
|
|
82
154
|
def invoke(
|
|
83
155
|
self,
|
|
@@ -86,47 +158,161 @@ class APIGateway:
|
|
|
86
158
|
auth: Optional[tuple[str, str]] = None,
|
|
87
159
|
**kwargs,
|
|
88
160
|
):
|
|
161
|
+
"""
|
|
162
|
+
Invoke the API gateway.
|
|
163
|
+
|
|
164
|
+
:param method: (str, optional) The HTTP method for the invocation.
|
|
165
|
+
:param headers: (dict, optional) The HTTP headers for the invocation.
|
|
166
|
+
:param auth: (Optional[tuple[str, str]], optional) The authentication creds for the invocation if required.
|
|
167
|
+
:param kwargs: (dict) Additional keyword arguments.
|
|
168
|
+
|
|
169
|
+
:return: The response from the API gateway invocation.
|
|
170
|
+
"""
|
|
89
171
|
if not self.invoke_url:
|
|
90
|
-
|
|
91
|
-
|
|
172
|
+
# try to resolve invoke_url before fail
|
|
173
|
+
self.sync()
|
|
174
|
+
if not self.invoke_url:
|
|
175
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
176
|
+
"Invocation url is not set. Set up gateway's `invoke_url` attribute."
|
|
177
|
+
)
|
|
178
|
+
if not self.is_ready():
|
|
179
|
+
raise mlrun.errors.MLRunPreconditionFailedError(
|
|
180
|
+
f"API gateway is not ready. " f"Current state: {self.state}"
|
|
92
181
|
)
|
|
182
|
+
|
|
93
183
|
if (
|
|
94
|
-
self.authentication_mode
|
|
184
|
+
self.authentication.authentication_mode
|
|
95
185
|
== NUCLIO_API_GATEWAY_AUTHENTICATION_MODE_BASIC_AUTH
|
|
96
186
|
and not auth
|
|
97
187
|
):
|
|
98
188
|
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
99
189
|
"API Gateway invocation requires authentication. Please pass credentials"
|
|
100
190
|
)
|
|
101
|
-
if auth:
|
|
102
|
-
headers["Authorization"] = self._generate_basic_auth(*auth)
|
|
103
191
|
return requests.request(
|
|
104
|
-
method=method,
|
|
192
|
+
method=method,
|
|
193
|
+
url=self.invoke_url,
|
|
194
|
+
headers=headers,
|
|
195
|
+
**kwargs,
|
|
196
|
+
auth=HTTPBasicAuth(*auth) if auth else None,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def wait_for_readiness(self, max_wait_time=90):
|
|
200
|
+
"""
|
|
201
|
+
Wait for the API gateway to become ready within the maximum wait time.
|
|
202
|
+
|
|
203
|
+
Parameters:
|
|
204
|
+
max_wait_time: int - Maximum time to wait in seconds (default is 90 seconds).
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
bool: True if the entity becomes ready within the maximum wait time, False otherwise
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
def _ensure_ready():
|
|
211
|
+
if not self.is_ready():
|
|
212
|
+
raise AssertionError(
|
|
213
|
+
f"Waiting for gateway readiness is taking more than {max_wait_time} seconds"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
return mlrun.utils.helpers.retry_until_successful(
|
|
217
|
+
3, max_wait_time, logger, False, _ensure_ready
|
|
105
218
|
)
|
|
106
219
|
|
|
220
|
+
def is_ready(self):
|
|
221
|
+
if self.state is not mlrun.common.schemas.api_gateway.APIGatewayState.ready:
|
|
222
|
+
# try to sync the state
|
|
223
|
+
self.sync()
|
|
224
|
+
return self.state == mlrun.common.schemas.api_gateway.APIGatewayState.ready
|
|
225
|
+
|
|
226
|
+
def sync(self):
|
|
227
|
+
"""
|
|
228
|
+
Synchronize the API gateway from the server.
|
|
229
|
+
"""
|
|
230
|
+
synced_gateway = mlrun.get_run_db().get_api_gateway(self.name, self.project)
|
|
231
|
+
synced_gateway = self.from_scheme(synced_gateway)
|
|
232
|
+
|
|
233
|
+
self.host = synced_gateway.host
|
|
234
|
+
self.path = synced_gateway.path
|
|
235
|
+
self.authentication = synced_gateway.authentication
|
|
236
|
+
self.functions = synced_gateway.functions
|
|
237
|
+
self.canary = synced_gateway.canary
|
|
238
|
+
self.description = synced_gateway.description
|
|
239
|
+
self.state = synced_gateway.state
|
|
240
|
+
|
|
241
|
+
def with_basic_auth(self, username: str, password: str):
|
|
242
|
+
"""
|
|
243
|
+
Set basic authentication for the API gateway.
|
|
244
|
+
|
|
245
|
+
:param username: (str) The username for basic authentication.
|
|
246
|
+
:param password: (str) The password for basic authentication.
|
|
247
|
+
"""
|
|
248
|
+
self.authentication = BasicAuth(username=username, password=password)
|
|
249
|
+
|
|
250
|
+
def with_canary(
|
|
251
|
+
self,
|
|
252
|
+
functions: Union[
|
|
253
|
+
list[str],
|
|
254
|
+
list[
|
|
255
|
+
Union[
|
|
256
|
+
RemoteRuntime,
|
|
257
|
+
ServingRuntime,
|
|
258
|
+
]
|
|
259
|
+
],
|
|
260
|
+
],
|
|
261
|
+
canary: list[int],
|
|
262
|
+
):
|
|
263
|
+
"""
|
|
264
|
+
Set canary function for the API gateway
|
|
265
|
+
|
|
266
|
+
:param functions: The list of functions associated with the API gateway
|
|
267
|
+
Can be a list of function names (["my-func1", "my-func2"])
|
|
268
|
+
or a list of nuclio functions of types
|
|
269
|
+
:py:class:`~mlrun.runtimes.nuclio.function.RemoteRuntime` OR
|
|
270
|
+
:py:class:`~mlrun.runtimes.nuclio.serving.ServingRuntime`
|
|
271
|
+
:param canary: The canary percents for the API gateway of type list[int]; for instance: [20,80]
|
|
272
|
+
|
|
273
|
+
"""
|
|
274
|
+
if len(functions) != 2:
|
|
275
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
276
|
+
f"Gateway with canary can be created only with two functions, "
|
|
277
|
+
f"the number of functions passed is {len(functions)}"
|
|
278
|
+
)
|
|
279
|
+
self.functions = self._validate_functions(self.project, functions)
|
|
280
|
+
self.canary = self._validate_canary(canary)
|
|
281
|
+
|
|
107
282
|
@classmethod
|
|
108
283
|
def from_scheme(cls, api_gateway: mlrun.common.schemas.APIGateway):
|
|
109
284
|
project = api_gateway.metadata.labels.get(PROJECT_NAME_LABEL)
|
|
110
285
|
functions, canary = cls._resolve_canary(api_gateway.spec.upstreams)
|
|
111
|
-
|
|
286
|
+
state = (
|
|
287
|
+
api_gateway.status.state
|
|
288
|
+
if api_gateway.status
|
|
289
|
+
else mlrun.common.schemas.APIGatewayState.none
|
|
290
|
+
)
|
|
291
|
+
api_gateway = cls(
|
|
112
292
|
project=project,
|
|
113
293
|
description=api_gateway.spec.description,
|
|
114
294
|
name=api_gateway.spec.name,
|
|
115
295
|
host=api_gateway.spec.host,
|
|
116
296
|
path=api_gateway.spec.path,
|
|
117
|
-
|
|
297
|
+
authentication=APIGatewayAuthenticator.from_scheme(api_gateway.spec),
|
|
118
298
|
functions=functions,
|
|
119
299
|
canary=canary,
|
|
120
300
|
)
|
|
301
|
+
api_gateway.state = state
|
|
302
|
+
return api_gateway
|
|
121
303
|
|
|
122
304
|
def to_scheme(self) -> mlrun.common.schemas.APIGateway:
|
|
123
305
|
upstreams = (
|
|
124
306
|
[
|
|
125
307
|
mlrun.common.schemas.APIGatewayUpstream(
|
|
126
|
-
nucliofunction={"name":
|
|
127
|
-
percentage=
|
|
128
|
-
)
|
|
129
|
-
|
|
308
|
+
nucliofunction={"name": self.functions[0]},
|
|
309
|
+
percentage=self.canary[0],
|
|
310
|
+
),
|
|
311
|
+
mlrun.common.schemas.APIGatewayUpstream(
|
|
312
|
+
# do not set percent for the second function,
|
|
313
|
+
# so we can define which function to display as a primary one in UI
|
|
314
|
+
nucliofunction={"name": self.functions[1]},
|
|
315
|
+
),
|
|
130
316
|
]
|
|
131
317
|
if self.canary
|
|
132
318
|
else [
|
|
@@ -141,27 +327,30 @@ class APIGateway:
|
|
|
141
327
|
spec=mlrun.common.schemas.APIGatewaySpec(
|
|
142
328
|
name=self.name,
|
|
143
329
|
description=self.description,
|
|
330
|
+
host=self.host,
|
|
144
331
|
path=self.path,
|
|
145
|
-
|
|
146
|
-
self.authentication_mode
|
|
332
|
+
authenticationMode=mlrun.common.schemas.APIGatewayAuthenticationMode.from_str(
|
|
333
|
+
self.authentication.authentication_mode
|
|
147
334
|
),
|
|
148
335
|
upstreams=upstreams,
|
|
149
336
|
),
|
|
150
337
|
)
|
|
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
|
-
)
|
|
338
|
+
api_gateway.spec.authentication = self.authentication.to_scheme()
|
|
158
339
|
return api_gateway
|
|
159
340
|
|
|
160
341
|
@property
|
|
161
342
|
def invoke_url(
|
|
162
343
|
self,
|
|
163
344
|
):
|
|
164
|
-
|
|
345
|
+
"""
|
|
346
|
+
Get the invoke URL.
|
|
347
|
+
|
|
348
|
+
:return: (str) The invoke URL.
|
|
349
|
+
"""
|
|
350
|
+
host = self.host
|
|
351
|
+
if not self.host.startswith("http"):
|
|
352
|
+
host = f"https://{self.host}"
|
|
353
|
+
return urljoin(host, self.path)
|
|
165
354
|
|
|
166
355
|
def _validate(
|
|
167
356
|
self,
|
|
@@ -180,8 +369,6 @@ class APIGateway:
|
|
|
180
369
|
],
|
|
181
370
|
],
|
|
182
371
|
canary: Optional[list[int]] = None,
|
|
183
|
-
username: Optional[str] = None,
|
|
184
|
-
password: Optional[str] = None,
|
|
185
372
|
):
|
|
186
373
|
if not name:
|
|
187
374
|
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
@@ -192,26 +379,23 @@ class APIGateway:
|
|
|
192
379
|
|
|
193
380
|
# validating canary
|
|
194
381
|
if canary:
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if sum(canary) != 100:
|
|
382
|
+
self._validate_canary(canary)
|
|
383
|
+
|
|
384
|
+
def _validate_canary(self, canary: list[int]):
|
|
385
|
+
if len(self.functions) != len(canary):
|
|
386
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
387
|
+
"Function and canary lists lengths do not match"
|
|
388
|
+
)
|
|
389
|
+
for canary_percent in canary:
|
|
390
|
+
if canary_percent < 0 or canary_percent > 100:
|
|
205
391
|
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
206
|
-
"The
|
|
392
|
+
"The percentage value must be in the range from 0 to 100"
|
|
207
393
|
)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if password and not username:
|
|
214
|
-
raise mlrun.errors.MLRunInvalidArgumentError("Username is not specified")
|
|
394
|
+
if sum(canary) != 100:
|
|
395
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
396
|
+
"The sum of canary function percents should be equal to 100"
|
|
397
|
+
)
|
|
398
|
+
return canary
|
|
215
399
|
|
|
216
400
|
@staticmethod
|
|
217
401
|
def _validate_functions(
|
|
@@ -257,17 +441,10 @@ class APIGateway:
|
|
|
257
441
|
f"input function {function_name} "
|
|
258
442
|
f"does not belong to this project"
|
|
259
443
|
)
|
|
260
|
-
|
|
444
|
+
nuclio_name = get_fullname(function_name, project, func.metadata.tag)
|
|
445
|
+
function_names.append(nuclio_name)
|
|
261
446
|
return function_names
|
|
262
447
|
|
|
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
448
|
@staticmethod
|
|
272
449
|
def _generate_basic_auth(username: str, password: str):
|
|
273
450
|
token = base64.b64encode(f"{username}:{password}".encode()).decode()
|