Skip to content

Commit cdb020b

Browse files
authored
Merge branch 'main' into main
2 parents fb80382 + fbed6b9 commit cdb020b

26 files changed

+2056
-920
lines changed

.codegen/__init__.py.tmpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ from databricks.sdk.mixins.workspace import WorkspaceExt
99
from databricks.sdk.service.{{.Package.Name}} import {{.PascalName}}API{{end}}
1010
from databricks.sdk.service.provisioning import Workspace
1111
from databricks.sdk import azure
12+
from typing import Optional
1213

1314
{{$args := list "host" "account_id" "username" "password" "client_id" "client_secret"
1415
"token" "profile" "config_file" "azure_workspace_resource_id" "azure_client_secret"

.codegen/_openapi_sha

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
d05898328669a3f8ab0c2ecee37db2673d3ea3f7
1+
248f4ad9668661da9d0bf4a7b0119a2d44fd1e75

.gitattributes

Lines changed: 0 additions & 275 deletions
Large diffs are not rendered by default.

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,34 @@
11
# Version changelog
22

3+
## [Release] Release v0.32.3
4+
5+
### New Features and Improvements
6+
7+
* Integrate Databricks SDK with Model Serving Auth Provider ([#761](https://github.com/databricks/databricks-sdk-py/pull/761)).
8+
9+
10+
### Bug Fixes
11+
12+
* Add DataPlane docs to the index ([#764](https://github.com/databricks/databricks-sdk-py/pull/764)).
13+
* `mypy` error: Skipping analyzing "google": module is installed, but missing library stubs or py.typed marker ([#769](https://github.com/databricks/databricks-sdk-py/pull/769)).
14+
15+
16+
17+
## [Release] Release v0.32.2
18+
19+
### New Features and Improvements
20+
21+
* Support Models in `dbutils.fs` operations ([#750](https://github.com/databricks/databricks-sdk-py/pull/750)).
22+
23+
24+
### Bug Fixes
25+
26+
* Do not specify --tenant flag when fetching managed identity access token from the CLI ([#748](https://github.com/databricks/databricks-sdk-py/pull/748)).
27+
* Fix deserialization of 401/403 errors ([#758](https://github.com/databricks/databricks-sdk-py/pull/758)).
28+
* Use correct optional typing in `WorkspaceClient` for `mypy` ([#760](https://github.com/databricks/databricks-sdk-py/pull/760)).
29+
30+
31+
332
## [Release] Release v0.32.1
433

534
### Bug Fixes

CONTRIBUTING.md

Lines changed: 12 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -18,54 +18,22 @@ Code style is enforced by a formatter check in your pull request. We use [yapf](
1818
## Signed Commits
1919
This repo requires all contributors to sign their commits. To configure this, you can follow [Github's documentation](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) to create a GPG key, upload it to your Github account, and configure your git client to sign commits.
2020

21-
## Sign your work
22-
The sign-off is a simple line at the end of the explanation for the patch. Your signature certifies that you wrote the patch or otherwise have the right to pass it on as an open-source patch. The rules are pretty simple: if you can certify the below (from developercertificate.org):
21+
## Developer Certificate of Origin
2322

24-
```
25-
Developer Certificate of Origin
26-
Version 1.1
27-
28-
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
29-
1 Letterman Drive
30-
Suite D4700
31-
San Francisco, CA, 94129
32-
33-
Everyone is permitted to copy and distribute verbatim copies of this
34-
license document, but changing it is not allowed.
35-
36-
37-
Developer's Certificate of Origin 1.1
38-
39-
By making a contribution to this project, I certify that:
40-
41-
(a) The contribution was created in whole or in part by me and I
42-
have the right to submit it under the open source license
43-
indicated in the file; or
23+
To contribute to this repository, you must sign off your commits to certify
24+
that you have the right to contribute the code and that it complies with the
25+
open source license. The rules are pretty simple, if you can certify the
26+
content of [DCO](./DCO), then simply add a "Signed-off-by" line to your
27+
commit message to certify your compliance. Please use your real name as
28+
pseudonymous/anonymous contributions are not accepted.
4429

45-
(b) The contribution is based upon previous work that, to the best
46-
of my knowledge, is covered under an appropriate open source
47-
license and I have the right under that license to submit that
48-
work with modifications, whether created in whole or in part
49-
by me, under the same open source license (unless I am
50-
permitted to submit under a different license), as indicated
51-
in the file; or
52-
53-
(c) The contribution was provided directly to me by some other
54-
person who certified (a), (b) or (c) and I have not modified
55-
it.
56-
57-
(d) I understand and agree that this project and the contribution
58-
are public and that a record of the contribution (including all
59-
personal information I submit with it, including my sign-off) is
60-
maintained indefinitely and may be redistributed consistent with
61-
this project or the open source license(s) involved.
30+
```
31+
Signed-off-by: Joe Smith <[email protected]>
6232
```
6333

64-
Then you just add a line to every git commit message:
34+
If you set your `user.name` and `user.email` git configs, you can sign your
35+
commit automatically with `git commit -s`:
6536

6637
```
67-
Signed-off-by: Joe Smith <[email protected]>
38+
git commit -s -m "Your commit message"
6839
```
69-
70-
If you set your `user.name` and `user.email` git configs, you can sign your commit automatically with git commit -s.
71-
You must use your real name (sorry, no pseudonyms or anonymous contributions).

DCO

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
Developer's Certificate of Origin 1.1
2+
3+
By making a contribution to this project, I certify that:
4+
5+
(a) The contribution was created in whole or in part by me and I
6+
have the right to submit it under the open source license
7+
indicated in the file; or
8+
9+
(b) The contribution is based upon previous work that, to the best
10+
of my knowledge, is covered under an appropriate open source
11+
license and I have the right under that license to submit that
12+
work with modifications, whether created in whole or in part
13+
by me, under the same open source license (unless I am
14+
permitted to submit under a different license), as indicated
15+
in the file; or
16+
17+
(c) The contribution was provided directly to me by some other
18+
person who certified (a), (b) or (c) and I have not modified
19+
it.
20+
21+
(d) I understand and agree that this project and the contribution
22+
are public and that a record of the contribution (including all
23+
personal information I submit with it, including my sign-off) is
24+
maintained indefinitely and may be redistributed consistent with
25+
this project or the open source license(s) involved.

databricks/sdk/__init__.py

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

databricks/sdk/credentials_provider.py

Lines changed: 130 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@
99
import platform
1010
import subprocess
1111
import sys
12+
import time
1213
from datetime import datetime
13-
from typing import Callable, Dict, List, Optional, Union
14+
from typing import Callable, Dict, List, Optional, Tuple, Union
1415

15-
import google.auth
16+
import google.auth # type: ignore
1617
import requests
17-
from google.auth import impersonated_credentials
18-
from google.auth.transport.requests import Request
19-
from google.oauth2 import service_account
18+
from google.auth import impersonated_credentials # type: ignore
19+
from google.auth.transport.requests import Request # type: ignore
20+
from google.oauth2 import service_account # type: ignore
2021

2122
from .azure import add_sp_management_token, add_workspace_id_header
2223
from .oauth import (ClientCredentials, OAuthClient, Refreshable, Token,
@@ -411,10 +412,7 @@ def _parse_expiry(expiry: str) -> datetime:
411412

412413
def refresh(self) -> Token:
413414
try:
414-
is_windows = sys.platform.startswith('win')
415-
# windows requires shell=True to be able to execute 'az login' or other commands
416-
# cannot use shell=True all the time, as it breaks macOS
417-
out = subprocess.run(self._cmd, capture_output=True, check=True, shell=is_windows)
415+
out = _run_subprocess(self._cmd, capture_output=True, check=True)
418416
it = json.loads(out.stdout.decode())
419417
expires_on = self._parse_expiry(it[self._expiry_field])
420418
return Token(access_token=it[self._access_token_field],
@@ -429,6 +427,26 @@ def refresh(self) -> Token:
429427
raise IOError(f'cannot get access token: {message}') from e
430428

431429

430+
def _run_subprocess(popenargs,
431+
input=None,
432+
capture_output=True,
433+
timeout=None,
434+
check=False,
435+
**kwargs) -> subprocess.CompletedProcess:
436+
"""Runs subprocess with given arguments.
437+
This handles OS-specific modifications that need to be made to the invocation of subprocess.run."""
438+
kwargs['shell'] = sys.platform.startswith('win')
439+
# windows requires shell=True to be able to execute 'az login' or other commands
440+
# cannot use shell=True all the time, as it breaks macOS
441+
logging.debug(f'Running command: {" ".join(popenargs)}')
442+
return subprocess.run(popenargs,
443+
input=input,
444+
capture_output=capture_output,
445+
timeout=timeout,
446+
check=check,
447+
**kwargs)
448+
449+
432450
class AzureCliTokenSource(CliTokenSource):
433451
""" Obtain the token granted by `az login` CLI command """
434452

@@ -437,13 +455,30 @@ def __init__(self, resource: str, subscription: Optional[str] = None, tenant: Op
437455
if subscription is not None:
438456
cmd.append("--subscription")
439457
cmd.append(subscription)
440-
if tenant:
458+
if tenant and not self.__is_cli_using_managed_identity():
441459
cmd.extend(["--tenant", tenant])
442460
super().__init__(cmd=cmd,
443461
token_type_field='tokenType',
444462
access_token_field='accessToken',
445463
expiry_field='expiresOn')
446464

465+
@staticmethod
466+
def __is_cli_using_managed_identity() -> bool:
467+
"""Checks whether the current CLI session is authenticated using managed identity."""
468+
try:
469+
cmd = ["az", "account", "show", "--output", "json"]
470+
out = _run_subprocess(cmd, capture_output=True, check=True)
471+
account = json.loads(out.stdout.decode())
472+
user = account.get("user")
473+
if user is None:
474+
return False
475+
return user.get("type") == "servicePrincipal" and user.get("name") in [
476+
'systemAssignedIdentity', 'userAssignedIdentity'
477+
]
478+
except subprocess.CalledProcessError as e:
479+
logger.debug("Failed to get account information from Azure CLI", exc_info=e)
480+
return False
481+
447482
def is_human_user(self) -> bool:
448483
"""The UPN claim is the username of the user, but not the Service Principal.
449484
@@ -664,6 +699,90 @@ def inner() -> Dict[str, str]:
664699
return inner
665700

666701

702+
# This Code is derived from Mlflow DatabricksModelServingConfigProvider
703+
# https://github.com/mlflow/mlflow/blob/1219e3ef1aac7d337a618a352cd859b336cf5c81/mlflow/legacy_databricks_cli/configure/provider.py#L332
704+
class ModelServingAuthProvider():
705+
_MODEL_DEPENDENCY_OAUTH_TOKEN_FILE_PATH = "/var/credentials-secret/model-dependencies-oauth-token"
706+
707+
def __init__(self):
708+
self.expiry_time = -1
709+
self.current_token = None
710+
self.refresh_duration = 300 # 300 Seconds
711+
712+
def should_fetch_model_serving_environment_oauth(self) -> bool:
713+
"""
714+
Check whether this is the model serving environment
715+
Additionally check if the oauth token file path exists
716+
"""
717+
718+
is_in_model_serving_env = (os.environ.get("IS_IN_DB_MODEL_SERVING_ENV")
719+
or os.environ.get("IS_IN_DATABRICKS_MODEL_SERVING_ENV") or "false")
720+
return (is_in_model_serving_env == "true"
721+
and os.path.isfile(self._MODEL_DEPENDENCY_OAUTH_TOKEN_FILE_PATH))
722+
723+
def get_model_dependency_oauth_token(self, should_retry=True) -> str:
724+
# Use Cached value if it is valid
725+
if self.current_token is not None and self.expiry_time > time.time():
726+
return self.current_token
727+
728+
try:
729+
with open(self._MODEL_DEPENDENCY_OAUTH_TOKEN_FILE_PATH) as f:
730+
oauth_dict = json.load(f)
731+
self.current_token = oauth_dict["OAUTH_TOKEN"][0]["oauthTokenValue"]
732+
self.expiry_time = time.time() + self.refresh_duration
733+
except Exception as e:
734+
# sleep and retry in case of any race conditions with OAuth refreshing
735+
if should_retry:
736+
logger.warning("Unable to read oauth token on first attmept in Model Serving Environment",
737+
exc_info=e)
738+
time.sleep(0.5)
739+
return self.get_model_dependency_oauth_token(should_retry=False)
740+
else:
741+
raise RuntimeError(
742+
"Unable to read OAuth credentials from the file mounted in Databricks Model Serving"
743+
) from e
744+
return self.current_token
745+
746+
def get_databricks_host_token(self) -> Optional[Tuple[str, str]]:
747+
if not self.should_fetch_model_serving_environment_oauth():
748+
return None
749+
750+
# read from DB_MODEL_SERVING_HOST_ENV_VAR if available otherwise MODEL_SERVING_HOST_ENV_VAR
751+
host = os.environ.get("DATABRICKS_MODEL_SERVING_HOST_URL") or os.environ.get(
752+
"DB_MODEL_SERVING_HOST_URL")
753+
token = self.get_model_dependency_oauth_token()
754+
755+
return (host, token)
756+
757+
758+
@credentials_strategy('model-serving', [])
759+
def model_serving_auth(cfg: 'Config') -> Optional[CredentialsProvider]:
760+
try:
761+
model_serving_auth_provider = ModelServingAuthProvider()
762+
if not model_serving_auth_provider.should_fetch_model_serving_environment_oauth():
763+
logger.debug("model-serving: Not in Databricks Model Serving, skipping")
764+
return None
765+
host, token = model_serving_auth_provider.get_databricks_host_token()
766+
if token is None:
767+
raise ValueError(
768+
"Got malformed auth (empty token) when fetching auth implicitly available in Model Serving Environment. Please contact Databricks support"
769+
)
770+
if cfg.host is None:
771+
cfg.host = host
772+
except Exception as e:
773+
logger.warning("Unable to get auth from Databricks Model Serving Environment", exc_info=e)
774+
return None
775+
776+
logger.info("Using Databricks Model Serving Authentication")
777+
778+
def inner() -> Dict[str, str]:
779+
# Call here again to get the refreshed token
780+
_, token = model_serving_auth_provider.get_databricks_host_token()
781+
return {"Authorization": f"Bearer {token}"}
782+
783+
return inner
784+
785+
667786
class DefaultCredentials:
668787
""" Select the first applicable credential provider from the chain """
669788

@@ -672,7 +791,7 @@ def __init__(self) -> None:
672791
self._auth_providers = [
673792
pat_auth, basic_auth, metadata_service, oauth_service_principal, azure_service_principal,
674793
github_oidc_azure, azure_cli, external_browser, databricks_cli, runtime_native_auth,
675-
google_credentials, google_id
794+
google_credentials, google_id, model_serving_auth
676795
]
677796

678797
def auth_type(self) -> str:

0 commit comments

Comments
 (0)