-
Notifications
You must be signed in to change notification settings - Fork 10
feat: Add Azure provider support #258
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
IkeM-L
wants to merge
16
commits into
finos:main
Choose a base branch
from
hmxlabs:IML/azure-provider
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
f162f35
feat: add Azure provider
70c94b7
Resolve type issues after rebase
7ad5b4b
fix: address PR comments
75958ca
chore: minimize Azure provider rebase surface
8522b2f
fix: align Azure provider with main registries
88e09cb
feat(azure): rebase based on Main not Common Code
958eaf3
chore: ruff
0a93855
fix: register auth
886ff89
fix: ruff check
9b5e2f9
fix: httpx dependency in Azure
738317a
fix: ruff and toml
11a0e57
fix: lazy loading and 3.11 dependencies
cfea023
fix: small issues
b1dacaf
fix: ci pyright
0395164
fix: rebase error handling and registration
4766a89
fix: split azure runtime and test dependencies
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| """Azure Provider implementation.""" | ||
|
|
||
| from orb.providers.azure.configuration.config import AzureProviderConfig | ||
| from orb.providers.azure.configuration.template_extension import AzureTemplateExtensionConfig | ||
| from orb.providers.azure.registration import ( | ||
| get_azure_extension_defaults, | ||
| initialize_azure_provider, | ||
| is_azure_provider_registered, | ||
| register_azure_extensions, | ||
| register_azure_template_factory, | ||
| ) | ||
|
|
||
| __all__: list[str] = [ | ||
| "AzureProviderConfig", | ||
| "AzureTemplateExtensionConfig", | ||
| "get_azure_extension_defaults", | ||
| "initialize_azure_provider", | ||
| "is_azure_provider_registered", | ||
| "register_azure_extensions", | ||
| "register_azure_template_factory", | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| """Azure authentication utilities.""" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| """Azure DefaultAzureCredential authentication strategy. | ||
|
|
||
| Uses the shared Azure infrastructure credential factory so auth and | ||
| provider-runtime flows construct the same credential shape. | ||
| """ | ||
|
|
||
| import asyncio | ||
| from typing import Any, Optional | ||
|
|
||
| from orb.domain.base.ports import LoggingPort | ||
| from orb.infrastructure.adapters.ports.auth import ( | ||
| AuthContext, | ||
| AuthPort, | ||
| AuthResult, | ||
| AuthStatus, | ||
| ) | ||
| from orb.infrastructure.di.injectable import injectable | ||
| from orb.providers.azure.infrastructure.credential_factory import ( | ||
| AsyncAzureAccessTokenProviderProtocol, | ||
| AsyncDefaultAzureAccessTokenProvider, | ||
| AzureAccessTokenProviderProtocol, | ||
| DefaultAzureAccessTokenProvider, | ||
| ) | ||
|
|
||
|
|
||
| @injectable | ||
| class AzureAuthStrategy(AuthPort): | ||
| """Authentication strategy using Azure DefaultAzureCredential.""" | ||
|
|
||
| def __init__( | ||
| self, | ||
| logger: LoggingPort, | ||
| client_id: Optional[str] = None, | ||
| enabled: bool = True, | ||
| token_provider: Optional[AzureAccessTokenProviderProtocol] = None, | ||
| async_token_provider: Optional[AsyncAzureAccessTokenProviderProtocol] = None, | ||
| ) -> None: | ||
| """Initialise Azure auth with async-first token acquisition.""" | ||
| self._logger = logger | ||
| self.client_id = client_id | ||
| self.enabled = enabled | ||
| self._token_provider = token_provider | ||
| self._async_token_provider = async_token_provider | ||
| if self._token_provider is None and self._async_token_provider is None: | ||
| self._async_token_provider = AsyncDefaultAzureAccessTokenProvider( | ||
| client_id=client_id, | ||
| logger=logger, | ||
| ) | ||
|
|
||
| @classmethod | ||
| def from_auth_config(cls, auth_config: Any) -> "AzureAuthStrategy": | ||
| """Build an Azure auth strategy from the shared AuthConfig object.""" | ||
| from orb.infrastructure.adapters.logging_adapter import LoggingAdapter | ||
|
|
||
| provider_auth = getattr(auth_config, "provider_auth", None) | ||
| azure_cfg = getattr(provider_auth, "azure", None) if provider_auth is not None else None | ||
| client_id = getattr(azure_cfg, "client_id", None) if azure_cfg is not None else None | ||
| enabled = bool(getattr(auth_config, "enabled", True)) | ||
| return cls( | ||
| logger=LoggingAdapter(), | ||
| client_id=client_id, | ||
| enabled=enabled, | ||
| ) | ||
|
|
||
| def _auth_error_types(self) -> tuple[type[Exception], ...]: | ||
| """Return the active provider's declared auth failure types.""" | ||
| if self._async_token_provider is not None: | ||
| return self._async_token_provider.get_auth_error_types() | ||
| if self._token_provider is None: | ||
| return DefaultAzureAccessTokenProvider( | ||
| client_id=self.client_id, | ||
| logger=self._logger, | ||
| ).get_auth_error_types() | ||
| return self._token_provider.get_auth_error_types() | ||
|
|
||
| async def _get_access_token(self, scope: str) -> str: | ||
| """Resolve an ARM token without blocking the event loop.""" | ||
| if self._async_token_provider is not None: | ||
| return await self._async_token_provider.get_access_token(scope) | ||
| if self._token_provider is None: | ||
| raise RuntimeError("Azure auth strategy has no token provider configured") | ||
| return await asyncio.to_thread(self._token_provider.get_access_token, scope) | ||
|
|
||
| async def authenticate(self, context: AuthContext) -> AuthResult: | ||
| if not self.enabled: | ||
| return AuthResult( | ||
| status=AuthStatus.FAILED, | ||
| error_message="Azure auth strategy disabled", | ||
| ) | ||
| try: | ||
| token = await self._get_access_token("https://management.azure.com/.default") | ||
|
|
||
| return AuthResult( | ||
| status=AuthStatus.SUCCESS, | ||
| user_id=self.client_id or "azure-identity", | ||
| token=token, | ||
| user_roles=["provider"], | ||
| metadata={ | ||
| "strategy": "azure_default_credential", | ||
| }, | ||
| ) | ||
| except self._auth_error_types() as exc: | ||
| self._logger.error("Azure authentication failed: %s", exc) | ||
| return AuthResult( | ||
| status=AuthStatus.FAILED, | ||
| error_message=f"Azure authentication failed: {exc}", | ||
| ) | ||
|
|
||
| async def validate_token(self, token: str) -> AuthResult: | ||
| """Token validation is handled by Azure SDK internally.""" | ||
| return AuthResult( | ||
| status=AuthStatus.SUCCESS, | ||
| token=token, | ||
| metadata={"strategy": "azure_default_credential"}, | ||
| ) | ||
|
|
||
| async def refresh_token(self, refresh_token: str) -> AuthResult: | ||
| """DefaultAzureCredential handles token refresh automatically.""" | ||
| try: | ||
| token = await self._get_access_token("https://management.azure.com/.default") | ||
| return AuthResult( | ||
| status=AuthStatus.SUCCESS, | ||
| token=token, | ||
| metadata={"strategy": "azure_default_credential", "refreshed": True}, | ||
| ) | ||
| except self._auth_error_types() as exc: | ||
| return AuthResult( | ||
| status=AuthStatus.FAILED, | ||
| error_message=f"Token refresh failed: {exc}", | ||
| ) | ||
|
|
||
| async def revoke_token(self, token: str) -> bool: | ||
| """Azure ARM tokens cannot be revoked directly.""" | ||
| self._logger.debug("Token revocation not supported for Azure ARM tokens") | ||
| return False | ||
|
|
||
| def get_strategy_name(self) -> str: | ||
| """Return the identifier for the Azure default-credential strategy.""" | ||
| return "azure_default_credential" | ||
|
|
||
| def is_enabled(self) -> bool: | ||
| """Return whether this auth strategy is currently enabled.""" | ||
| return self.enabled |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| """Shared Azure provider capability metadata.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from copy import deepcopy | ||
| from typing import Any | ||
|
|
||
| from orb.providers.azure.domain.template.value_objects import AzureProviderApi | ||
|
|
||
| _AZURE_API_CAPABILITIES: dict[str, dict[str, Any]] = { | ||
| AzureProviderApi.VMSS.value: { | ||
| "supported_fleet_types": [], | ||
| "supports_spot": True, | ||
| "supports_on_demand": True, | ||
| "max_instances": 1000, | ||
| }, | ||
| AzureProviderApi.VMSS_UNIFORM.value: { | ||
| "supported_fleet_types": [], | ||
| "supports_spot": True, | ||
| "supports_on_demand": True, | ||
| "max_instances": 1000, | ||
| }, | ||
| AzureProviderApi.SINGLE_VM.value: { | ||
| "supported_fleet_types": [], | ||
| "supports_spot": True, | ||
| "supports_on_demand": True, | ||
| "max_instances": 1000, | ||
| }, | ||
| AzureProviderApi.CYCLECLOUD.value: { | ||
| "supported_fleet_types": [], | ||
| "supports_spot": False, | ||
| "supports_on_demand": True, | ||
| "requires_existing_cluster": True, | ||
| "required_create_fields": ["cluster_name", "node_array"], | ||
| "capacity_limit_source": "cluster_status.maxCount", | ||
| "supports_async_operations": True, | ||
| }, | ||
| } | ||
|
|
||
|
|
||
| def get_supported_api_capabilities() -> dict[str, dict[str, Any]]: | ||
| """Return Azure API capability metadata.""" | ||
| return deepcopy(_AZURE_API_CAPABILITIES) | ||
|
|
||
|
|
||
| def get_supported_apis() -> list[str]: | ||
| """Return the canonical list of Azure provider APIs.""" | ||
| return list(_AZURE_API_CAPABILITIES.keys()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| """Azure CLI integration helpers.""" |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After rebase, please put the Azure SDKs under an
[azure]optional-dependencies extra rather than the core[project.dependencies]list. Main has the pattern in place:[aws]exists as an opt-in alias for boto3,[all-providers]is a meta-extra. With 9 azure-* SDKs in core, every non-Azure deployment pulls in ~hundreds of MB of unused transitive deps. Addazure = [...]under[project.optional-dependencies]and extendall-providersto["orb-py[aws,azure]"].