Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
## Release v0.92.0

### New Features and Improvements
* Change default environment to cloud-agnostic. Unknown hosts now use Cloud.UNKNOWN instead of assuming AWS, enabling better support for cloud-agnostic endpoints.

### Security

### Bug Fixes

### Documentation
* Added "Retries" section to README.
* Unified Private Link error messages across all clouds. Private Link validation errors now provide guidance for AWS, Azure, and GCP in a single message.

### Internal Changes

Expand Down
13 changes: 9 additions & 4 deletions databricks/sdk/environments.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,20 @@ class Cloud(Enum):
AWS = "AWS"
AZURE = "AZURE"
GCP = "GCP"
UNKNOWN = "UNKNOWN"


@dataclass
class DatabricksEnvironment:
cloud: Cloud
dns_zone: str
dns_zone: Optional[str] = None
azure_application_id: Optional[str] = None
azure_environment: Optional[AzureEnvironment] = None

def deployment_url(self, name: str) -> str:
# Unified environments do not have a separate workspace host.
if self.dns_zone is None:
raise ValueError("This environment does not support deployment URLs.")
return f"https://{name}{self.dns_zone}"

@property
Expand All @@ -70,13 +74,13 @@ def azure_active_directory_endpoint(self) -> Optional[str]:
return self.azure_environment.active_directory_endpoint


DEFAULT_ENVIRONMENT = DatabricksEnvironment(Cloud.AWS, ".cloud.databricks.com")
DEFAULT_ENVIRONMENT = DatabricksEnvironment(Cloud.UNKNOWN, None)

ALL_ENVS = [
DatabricksEnvironment(Cloud.AWS, ".dev.databricks.com"),
DatabricksEnvironment(Cloud.AWS, ".staging.cloud.databricks.com"),
DatabricksEnvironment(Cloud.AWS, ".cloud.databricks.us"),
DEFAULT_ENVIRONMENT,
DatabricksEnvironment(Cloud.AWS, ".cloud.databricks.com"),
DatabricksEnvironment(
Cloud.AZURE,
".dev.azuredatabricks.net",
Expand Down Expand Up @@ -110,13 +114,14 @@ def azure_active_directory_endpoint(self) -> Optional[str]:
DatabricksEnvironment(Cloud.GCP, ".dev.gcp.databricks.com"),
DatabricksEnvironment(Cloud.GCP, ".staging.gcp.databricks.com"),
DatabricksEnvironment(Cloud.GCP, ".gcp.databricks.com"),
DEFAULT_ENVIRONMENT,
]


def get_environment_for_hostname(hostname: Optional[str]) -> DatabricksEnvironment:
if not hostname:
return DEFAULT_ENVIRONMENT
for env in ALL_ENVS:
if hostname.endswith(env.dns_zone):
if env.dns_zone and hostname.endswith(env.dns_zone):
return env
return DEFAULT_ENVIRONMENT
2 changes: 1 addition & 1 deletion databricks/sdk/errors/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,6 @@ def get_api_error(self, response: requests.Response) -> Optional[DatabricksError
# Private link failures happen via a redirect to the login page. From a requests-perspective, the request
# is successful, but the response is not what we expect. We need to handle this case separately.
if _is_private_link_redirect(response):
return _get_private_link_validation_error(response.url)
return _get_private_link_validation_error()

return None
52 changes: 15 additions & 37 deletions databricks/sdk/errors/private_link.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,23 @@
from dataclasses import dataclass
from urllib import parse

import requests

from ..environments import Cloud, get_environment_for_hostname
from .platform import PermissionDenied


@dataclass
class _PrivateLinkInfo:
serviceName: str
endpointName: str
referencePage: str

def error_message(self):
return (
f"The requested workspace has {self.serviceName} enabled and is not accessible from the current network. "
f"Ensure that {self.serviceName} is properly configured and that your device has access to the "
f"{self.endpointName}. For more information, see {self.referencePage}."
)


_private_link_info_map = {
Cloud.AWS: _PrivateLinkInfo(
serviceName="AWS PrivateLink",
endpointName="AWS VPC endpoint",
referencePage="https://docs.databricks.com/en/security/network/classic/privatelink.html",
),
Cloud.AZURE: _PrivateLinkInfo(
serviceName="Azure Private Link",
endpointName="Azure Private Link endpoint",
referencePage="https://learn.microsoft.com/en-us/azure/databricks/security/network/classic/private-link-standard#authentication-troubleshooting",
),
Cloud.GCP: _PrivateLinkInfo(
serviceName="Private Service Connect",
endpointName="GCP VPC endpoint",
referencePage="https://docs.gcp.databricks.com/en/security/network/classic/private-service-connect.html",
),
}
def _unified_private_link_error_message():
"""Returns a unified error message for private link validation errors across all clouds."""
return (
"The requested workspace has Private Link enabled and is not accessible from the current network. "
"Ensure that Private Link is properly configured for your cloud provider and that your device has "
"access to the appropriate endpoint:\n\n"
" • AWS: Ensure AWS PrivateLink is configured and you have access to the AWS VPC endpoint. "
"See https://docs.databricks.com/en/security/network/classic/privatelink.html\n"
" • Azure: Ensure Azure Private Link is configured and you have access to the Azure Private Link endpoint. "
"See https://learn.microsoft.com/en-us/azure/databricks/security/network/classic/private-link-standard#authentication-troubleshooting\n"
" • GCP: Ensure Private Service Connect is configured and you have access to the GCP VPC endpoint. "
"See https://docs.gcp.databricks.com/en/security/network/classic/private-service-connect.html"
)


class PrivateLinkValidationError(PermissionDenied):
Expand All @@ -50,11 +30,9 @@ def _is_private_link_redirect(resp: requests.Response) -> bool:
return parsed.path == "/login.html" and "error=private-link-validation-error" in parsed.query


def _get_private_link_validation_error(url: str) -> PrivateLinkValidationError:
parsed = parse.urlparse(url)
env = get_environment_for_hostname(parsed.hostname)
def _get_private_link_validation_error() -> PrivateLinkValidationError:
return PrivateLinkValidationError(
message=_private_link_info_map[env.cloud].error_message(),
message=_unified_private_link_error_message(),
error_code="PRIVATE_LINK_VALIDATION_ERROR",
status_code=403,
)
40 changes: 40 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from databricks.sdk import AccountClient, WorkspaceClient, oauth, useragent
from databricks.sdk.config import (ClientType, Config, HostType, with_product,
with_user_agent_extra)
from databricks.sdk.environments import Cloud
from databricks.sdk.version import __version__

from .conftest import noop_credentials, set_az_path, set_home
Expand Down Expand Up @@ -782,3 +783,42 @@ def test_oidc_scopes_sent_to_token_endpoint(requests_mock, tmp_path, scopes_inpu
config.authenticate()

assert _get_scope_from_request(token_mock.last_request.text) == expected_scope


def test_unknown_cloud_environment_properties(mocker):
"""Test that is_azure/is_gcp/is_aws properties work with Cloud.UNKNOWN environment."""
mocker.patch("databricks.sdk.config.Config.init_auth")

config = Config(
host="https://unknown-host.databricks.com",
token="test-token",
)

# Unknown host should have UNKNOWN cloud
assert config.environment.cloud == Cloud.UNKNOWN

# All cloud properties should return False without crashing
assert config.is_azure is False
assert config.is_gcp is False
assert config.is_aws is False


def test_azure_resource_id_sets_is_azure_with_unknown_environment(mocker):
"""Test that azure_workspace_resource_id sets is_azure even when environment is UNKNOWN."""
mocker.patch("databricks.sdk.config.Config.init_auth")

config = Config(
host="https://unknown-host.databricks.com",
azure_workspace_resource_id="/subscriptions/test/resourceGroups/test/providers/Microsoft.Databricks/workspaces/test",
azure_client_id="test-client-id",
azure_tenant_id="test-tenant-id",
azure_client_secret="test-secret",
)

# Should have UNKNOWN cloud
assert config.environment.cloud == Cloud.UNKNOWN

# is_azure should still be True due to azure_workspace_resource_id
assert config.is_azure is True
assert config.is_gcp is False
assert config.is_aws is False
17 changes: 11 additions & 6 deletions tests/test_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ def fake_valid_response(

def make_private_link_response() -> requests.Response:
resp = requests.Response()
resp.url = "https://databricks.com/login.html?error=private-link-validation-error"
resp.request = requests.Request("GET", "https://databricks.com/api/2.0/service").prepare()
resp.url = "https://cloud.databricks.com/login.html?error=private-link-validation-error"
resp.request = requests.Request("GET", "https://cloud.databricks.com/api/2.0/service").prepare()
resp._content = b"{}"
resp.status_code = 200
return resp
Expand Down Expand Up @@ -277,10 +277,15 @@ class TestCase:
response=make_private_link_response(),
want_err_type=errors.PrivateLinkValidationError,
want_message=(
"The requested workspace has AWS PrivateLink enabled and is not accessible from the current network. "
"Ensure that AWS PrivateLink is properly configured and that your device has access to the AWS VPC "
"endpoint. For more information, see "
"https://docs.databricks.com/en/security/network/classic/privatelink.html."
"The requested workspace has Private Link enabled and is not accessible from the current network. "
"Ensure that Private Link is properly configured for your cloud provider and that your device has "
"access to the appropriate endpoint:\n\n"
" • AWS: Ensure AWS PrivateLink is configured and you have access to the AWS VPC endpoint. "
"See https://docs.databricks.com/en/security/network/classic/privatelink.html\n"
" • Azure: Ensure Azure Private Link is configured and you have access to the Azure Private Link endpoint. "
"See https://learn.microsoft.com/en-us/azure/databricks/security/network/classic/private-link-standard#authentication-troubleshooting\n"
" • GCP: Ensure Private Service Connect is configured and you have access to the GCP VPC endpoint. "
"See https://docs.gcp.databricks.com/en/security/network/classic/private-service-connect.html"
),
),
TestCase(
Expand Down
Loading