diff --git a/.changes/unreleased/optimization-20260325-161506.yaml b/.changes/unreleased/optimization-20260325-161506.yaml new file mode 100644 index 00000000..1dde5085 --- /dev/null +++ b/.changes/unreleased/optimization-20260325-161506.yaml @@ -0,0 +1,8 @@ +kind: optimization +body: Add deprecation warning for `DefaultAzureCredential` fallback with migration guidance to explicit `token_credential` parameter +time: 2026-03-25T16:15:06.677907+02:00 +custom: + Author: shirasassoon + AuthorLink: https://github.com/shirasassoon + Issue: "903" + IssueLink: https://github.com/microsoft/fabric-cicd/issues/903 diff --git a/devtools/debug_api.py b/devtools/debug_api.py index d95c0f47..fbd98034 100644 --- a/devtools/debug_api.py +++ b/devtools/debug_api.py @@ -3,7 +3,7 @@ # The following is intended for developers of fabric-cicd to debug and call Fabric REST APIs locally from the github repo -from azure.identity import ClientSecretCredential, DefaultAzureCredential +from azure.identity import AzureCliCredential, AzurePowerShellCredential, ClientSecretCredential from fabric_cicd import change_log_level, constants from fabric_cicd._common._fabric_endpoint import FabricEndpoint @@ -13,20 +13,21 @@ # change_log_level() if __name__ == "__main__": - # Replace None correct value when using SPN auth + # Azure CLI auth - comment out to use a different auth method + token_credential = AzureCliCredential() + + # Uncomment to use PowerShell auth + # token_credential = AzurePowerShellCredential() + + # Uncomment to use SPN auth # client_id = "your-client-id" # client_secret = "your-client-secret" # tenant_id = "your-tenant-id" - token_credential = ( - None # ClientSecretCredential(client_id=client_id, client_secret=client_secret, tenant_id=tenant_id) - ) + # token_credential = ClientSecretCredential(client_id=client_id, client_secret=client_secret, tenant_id=tenant_id) # Create endpoint object - fe = FabricEndpoint( # if credential is not defined, use DefaultAzureCredential - token_credential=( - DefaultAzureCredential() if token_credential is None else validate_token_credential(token_credential) - ) - ) + fe = FabricEndpoint(token_credential=validate_token_credential(token_credential)) + # Set workspace id variable if needed in API url workspace_id = "8f5c0cec-a8ea-48cd-9da4-871dc2642f4c" diff --git a/devtools/debug_local config.py b/devtools/debug_local config.py index a92a7810..bbc79224 100644 --- a/devtools/debug_local config.py +++ b/devtools/debug_local config.py @@ -6,7 +6,7 @@ import sys from pathlib import Path -from azure.identity import ClientSecretCredential +from azure.identity import AzureCliCredential, AzurePowerShellCredential, ClientSecretCredential root_directory = Path(__file__).resolve().parent.parent sys.path.insert(0, str(root_directory / "src")) @@ -19,6 +19,12 @@ # In this example, the config file sits within the root/sample/workspace directory config_file = str(root_directory / "sample" / "workspace" / "config.yml") +# Azure CLI auth - comment out to use a different auth method +token_credential = AzureCliCredential() + +# Uncomment to use PowerShell auth +# token_credential = AzurePowerShellCredential() + # Uncomment to use SPN auth # client_id = "your-client-id" # client_secret = "your-client-secret" @@ -31,8 +37,8 @@ config_file_path=config_file, # Comment out if environment is not needed environment="dev", - # Uncomment to use SPN auth - # token_credential=token_credential, + # Explicit token credential required for auth (choose one of the options above) + token_credential=token_credential, # Uncomment to override specific config values (pass in a dictionary of override values) # config_override=config_override_dict ) diff --git a/devtools/debug_local.py b/devtools/debug_local.py index 07e68789..052e1ac8 100644 --- a/devtools/debug_local.py +++ b/devtools/debug_local.py @@ -6,7 +6,7 @@ import sys from pathlib import Path -from azure.identity import ClientSecretCredential +from azure.identity import AzureCliCredential, AzurePowerShellCredential, ClientSecretCredential root_directory = Path(__file__).resolve().parent.parent sys.path.insert(0, str(root_directory / "src")) @@ -51,6 +51,12 @@ "SparkJobDefinition", ] +# Azure CLI auth - comment out to use a different auth method +token_credential = AzureCliCredential() + +# Uncomment to use PowerShell auth +# token_credential = AzurePowerShellCredential() + # Uncomment to use SPN auth # client_id = "your-client-id" # client_secret = "your-client-secret" @@ -65,8 +71,8 @@ environment=environment, repository_directory=repository_directory, item_type_in_scope=item_type_in_scope, - # Uncomment to use SPN auth - # token_credential=token_credential, + # Explicit token credential required for auth (choose one of the options above) + token_credential=token_credential, ) # Uncomment to publish diff --git a/devtools/debug_trace_deployment.py b/devtools/debug_trace_deployment.py index c9735362..d4f59f25 100644 --- a/devtools/debug_trace_deployment.py +++ b/devtools/debug_trace_deployment.py @@ -9,6 +9,8 @@ import sys from pathlib import Path +from azure.identity import AzureCliCredential + root_directory = Path(__file__).resolve().parent.parent sys.path.insert(0, str(root_directory / "src")) @@ -27,7 +29,6 @@ def main(): raise ValueError(msg) environment = "PPE" - repository_directory = str(root_directory / "sample" / "workspace") item_type_in_scope = [ "Dataflow", @@ -49,6 +50,7 @@ def main(): "VariableLibrary", "Warehouse", ] + token_credential = AzureCliCredential() for flag in ["enable_shortcut_publish", "continue_on_shortcut_failure"]: fabric_cicd.append_feature_flag(flag) target_workspace = fabric_cicd.FabricWorkspace( @@ -56,6 +58,7 @@ def main(): environment=environment, repository_directory=str(repository_directory), item_type_in_scope=item_type_in_scope, + token_credential=token_credential, ) fabric_cicd.publish_all_items(target_workspace) diff --git a/docs/example/authentication.md b/docs/example/authentication.md index f1597a32..401aa676 100644 --- a/docs/example/authentication.md +++ b/docs/example/authentication.md @@ -1,18 +1,26 @@ # Authentication Examples -The following are the most common authentication flows for fabric-cicd. However, because fabric-cicd supports any [TokenCredential](https://learn.microsoft.com/en-us/dotnet/api/azure.core.tokencredential), there are multiple authentication methods available beyond the ones described here. +The following are the most common authentication flows for fabric-cicd. However, because fabric-cicd supports any [TokenCredential](https://learn.microsoft.com/en-us/dotnet/api/azure.core.tokencredential), there are multiple authentication methods available beyond the ones described here. These examples provide starting points that should be adapted for your specific environment and security requirements. -## Default Credential +> **⚠️ DEPRECATION NOTICE:** Due to security best practices, the **Default Credential** (`DefaultAzureCredential`) authentication method is deprecated and will be removed in a future release. Please migrate to one of the explicit credential methods described below. -This approach utilizes the default credential flow, meaning no explicit TokenCredential is provided. It is the most common authentication method and is particularly useful with deployments where authentication is defined outside of this execution. +**Notes:** + +- Explicit `token_credential` parameter is used for all scenarios except Fabric Notebook runtime, which handles authentication automatically. +- Avoid hardcoding credentials. Use environment variables or secret management services. SPN + Secret auth can also be achieved via `az login --service-principal` or `Connect-AzAccount -ServicePrincipal` in the CLI/PowerShell flows below. + +## CLI Credential + +This approach utilizes the CLI credential flow, meaning it only refers to the authentication established with `az login`. This is agnostic of the executing user; it can be UPN, SPN, Managed Identity, etc. Whatever is used to log in will be used. === "Local" ```python - '''Log in with Azure CLI (az login) or Azure PowerShell (Connect-AzAccount) prior to execution''' + '''Log in with Azure CLI (az login) prior to execution''' from pathlib import Path + from azure.identity import AzureCliCredential from fabric_cicd import FabricWorkspace, publish_all_items, unpublish_all_orphan_items # Assumes your script is one level down from root @@ -24,12 +32,16 @@ This approach utilizes the default credential flow, meaning no explicit TokenCre repository_directory = str(root_directory / "your-workspace-directory") item_type_in_scope = ["Notebook", "DataPipeline", "Environment"] + # Use Azure CLI credential to authenticate + token_credential = AzureCliCredential() + # Initialize the FabricWorkspace object with the required parameters target_workspace = FabricWorkspace( workspace_id=workspace_id, environment=environment, repository_directory=repository_directory, item_type_in_scope=item_type_in_scope, + token_credential=token_credential, ) # Publish all items defined in item_type_in_scope @@ -43,14 +55,15 @@ This approach utilizes the default credential flow, meaning no explicit TokenCre ```python ''' - Log in with Azure CLI (az login) or Azure PowerShell (Connect-AzAccount) prior to execution - OR (Preferred) Use Az CLI or AzPowerShell ADO Tasks with a Service Connection + Log in with Azure CLI (az login) prior to execution + OR (Preferred) Use Az CLI ADO Tasks with a Service Connection ''' import sys import os from pathlib import Path + from azure.identity import AzureCliCredential from fabric_cicd import FabricWorkspace, publish_all_items, unpublish_all_orphan_items, change_log_level # Force unbuffered output like `python -u` @@ -70,12 +83,16 @@ This approach utilizes the default credential flow, meaning no explicit TokenCre repository_directory = str(root_directory / "your-workspace-directory") item_type_in_scope = ["Notebook", "DataPipeline", "Environment"] + # Use Azure CLI credential to authenticate + token_credential = AzureCliCredential() + # Initialize the FabricWorkspace object with the required parameters target_workspace = FabricWorkspace( workspace_id=workspace_id, environment=environment, repository_directory=repository_directory, item_type_in_scope=item_type_in_scope, + token_credential=token_credential, ) # Publish all items defined in item_type_in_scope @@ -88,68 +105,56 @@ This approach utilizes the default credential flow, meaning no explicit TokenCre === "GitHub" ```python - '''Unconfirmed example at this time, however, the Azure DevOps example is a good starting point''' - ``` - -=== "Fabric Notebook" + ''' + Log in with Azure CLI (az login) prior to execution + Requires: azure/login workflow step in GitHub Actions + ''' - ```python - '''fabric-cicd will automatically generate a TokenCredential based on the user session context.''' - - import tempfile - import subprocess import os + from pathlib import Path + from azure.identity import AzureCliCredential from fabric_cicd import FabricWorkspace, publish_all_items, unpublish_all_orphan_items - - # Sample configuration values - workspace_id = "f3240389-4fbf-4509-83b2-25583d2d6ea0" - repo_url = "https://github.com/microsoft/fabric-cicd.git" - repo_ref = "main" - workspace_path = "sample/workspace" - - # Create a temporary directory - temp_dir = tempfile.mkdtemp(prefix="cloned_repo_") - print(f"Created temporary directory: {temp_dir}") - - # Clone the specific branch - print(f"Cloning {repo_url} (ref: {repo_ref})...") - result = subprocess.run( - ["git", "clone", "--branch", repo_ref, "--single-branch", repo_url, temp_dir], - capture_output=True, - text=True - ) - - workspace_root = os.path.join(temp_dir, workspace_path) - - # Deploy workspace items from cloned repository - item_type_in_scope = ["VariableLibrary"] # "" , "SparkJobDefinition", "Lakehouse", "Environment", - + + # GitHub Actions sets GITHUB_WORKSPACE automatically + root_directory = Path(os.getenv("GITHUB_WORKSPACE", ".")).resolve() + + # Sample values for FabricWorkspace parameters + workspace_id = os.getenv("WORKSPACE_ID") + environment = os.getenv("ENVIRONMENT", "PROD") + repository_directory = str(root_directory / "your-workspace-directory") + item_type_in_scope = ["Notebook", "DataPipeline", "Environment"] + + # Use Azure CLI credential (assumes 'az login' in workflow step) + token_credential = AzureCliCredential() + # Initialize the FabricWorkspace object with the required parameters target_workspace = FabricWorkspace( workspace_id=workspace_id, - repository_directory=workspace_root, - item_type_in_scope=item_type_in_scope + environment=environment, + repository_directory=repository_directory, + item_type_in_scope=item_type_in_scope, + token_credential=token_credential, ) - + # Publish all items defined in item_type_in_scope publish_all_items(target_workspace) - + # Unpublish all items defined in item_type_in_scope not found in repository unpublish_all_orphan_items(target_workspace) ``` -## CLI Credential +## AZ PowerShell Credential -This approach utilizes the CLI credential flow, meaning it only refers to the authentication established with az login. This is agnostic of the executing user, it can be UPN, SPN, Managed Identity, etc. Whatever is used to log in will be used. +This approach utilizes the AZ PowerShell credential flow, meaning it only refers to the authentication established with `Connect-AzAccount`. This is agnostic of the executing user; it can be UPN, SPN, Managed Identity, etc. Whatever is used to log in will be used. === "Local" ```python - '''Log in with Azure CLI (az login) prior to execution''' + '''Log in with Azure PowerShell (Connect-AzAccount) prior to execution''' from pathlib import Path - from azure.identity import AzureCliCredential + from azure.identity import AzurePowerShellCredential from fabric_cicd import FabricWorkspace, publish_all_items, unpublish_all_orphan_items # Assumes your script is one level down from root @@ -161,8 +166,8 @@ This approach utilizes the CLI credential flow, meaning it only refers to the au repository_directory = str(root_directory / "your-workspace-directory") item_type_in_scope = ["Notebook", "DataPipeline", "Environment"] - # Use Azure CLI credential to authenticate - token_credential = AzureCliCredential() + # Use Azure PowerShell credential to authenticate + token_credential = AzurePowerShellCredential() # Initialize the FabricWorkspace object with the required parameters target_workspace = FabricWorkspace( @@ -184,15 +189,15 @@ This approach utilizes the CLI credential flow, meaning it only refers to the au ```python ''' - Log in with Azure CLI (az login) prior to execution - OR (Preferred) Use Az CLI ADO Tasks with a Service Connection + Log in with Azure PowerShell (Connect-AzAccount) prior to execution + OR (Preferred) Use AzPowerShell ADO Tasks with a Service Connection ''' import sys import os from pathlib import Path - from azure.identity import AzureCliCredential + from azure.identity import AzurePowerShellCredential from fabric_cicd import FabricWorkspace, publish_all_items, unpublish_all_orphan_items, change_log_level # Force unbuffered output like `python -u` @@ -212,8 +217,8 @@ This approach utilizes the CLI credential flow, meaning it only refers to the au repository_directory = str(root_directory / "your-workspace-directory") item_type_in_scope = ["Notebook", "DataPipeline", "Environment"] - # Use Azure CLI credential to authenticate - token_credential = AzureCliCredential() + # Use Azure PowerShell credential to authenticate + token_credential = AzurePowerShellCredential() # Initialize the FabricWorkspace object with the required parameters target_workspace = FabricWorkspace( @@ -234,39 +239,26 @@ This approach utilizes the CLI credential flow, meaning it only refers to the au === "GitHub" ```python - '''Unconfirmed example at this time, however, the Azure DevOps example is a good starting point''' - ``` - -=== "Fabric Notebook" - - ```python - '''Unconfirmed example at this time, Default Credential flow is recommended''' - ``` - -## AZ PowerShell Credential - -This approach utilizes the AZ PowerShell credential flow, meaning it only refers to the authentication established with Connect-AzAccount. This is agnostic of the executing user, it can be UPN, SPN, Managed Identity, etc. Whatever is used to log in will be used. - -=== "Local" - - ```python - '''Log in with Azure PowerShell (Connect-AzAccount) prior to execution''' + ''' + Log in with Azure PowerShell (Connect-AzAccount) prior to execution + Requires: azure/powershell workflow step in GitHub Actions + ''' + import os from pathlib import Path - from azure.identity import AzurePowerShellCredential from fabric_cicd import FabricWorkspace, publish_all_items, unpublish_all_orphan_items - # Assumes your script is one level down from root - root_directory = Path(__file__).resolve().parent + # GitHub Actions sets GITHUB_WORKSPACE automatically + root_directory = Path(os.getenv("GITHUB_WORKSPACE", ".")).resolve() # Sample values for FabricWorkspace parameters - workspace_id = "your-workspace-id" - environment = "your-environment" + workspace_id = os.getenv("WORKSPACE_ID") + environment = os.getenv("ENVIRONMENT", "PROD") repository_directory = str(root_directory / "your-workspace-directory") item_type_in_scope = ["Notebook", "DataPipeline", "Environment"] - # Use Azure CLI credential to authenticate + # Use Azure PowerShell credential (assumes 'Connect-AzAccount' in workflow step) token_credential = AzurePowerShellCredential() # Initialize the FabricWorkspace object with the required parameters @@ -285,19 +277,23 @@ This approach utilizes the AZ PowerShell credential flow, meaning it only refers unpublish_all_orphan_items(target_workspace) ``` +## Managed Identity Credential + +This approach uses Azure Managed Identity, eliminating the need to manage secrets. Managed identities provide an automatically managed identity in Azure AD for applications to use when connecting to resources. + === "Azure DevOps" ```python ''' - Log in with Azure PowerShell (Connect-AzAccount) prior to execution - OR (Preferred) Use AzPowerShell ADO Tasks with a Service Connection + Running on Azure DevOps self-hosted agents with system-assigned managed identity + OR Azure DevOps agents hosted on Azure VMs with managed identity ''' import sys import os from pathlib import Path - from azure.identity import AzurePowerShellCredential + from azure.identity import ManagedIdentityCredential from fabric_cicd import FabricWorkspace, publish_all_items, unpublish_all_orphan_items, change_log_level # Force unbuffered output like `python -u` @@ -317,8 +313,8 @@ This approach utilizes the AZ PowerShell credential flow, meaning it only refers repository_directory = str(root_directory / "your-workspace-directory") item_type_in_scope = ["Notebook", "DataPipeline", "Environment"] - # Use Azure CLI credential to authenticate - token_credential = AzurePowerShellCredential() + # Use system-assigned managed identity + token_credential = ManagedIdentityCredential() # Initialize the FabricWorkspace object with the required parameters target_workspace = FabricWorkspace( @@ -340,44 +336,26 @@ This approach utilizes the AZ PowerShell credential flow, meaning it only refers ```python ''' - Unconfirmed example at this time, however, the Azure DevOps example is a good starting point + Running on GitHub self-hosted runners with system-assigned managed identity + OR GitHub Actions hosted on Azure VMs with managed identity ''' - ``` - -=== "Fabric Notebook" - - ```python - '''Unconfirmed example at this time, Default Credential flow is recommended''' - ``` - -## Explicit SPN Secret Credential - -This approach utilizes directly passing in SPN Client Id and Client Secret. Although you can pass in directly, it's not recommended and should store this outside of your code. It's important to consider that SPN + Secret is still possible to leverage in the above AZ PowerShell and AZ CLI flows - -=== "Local" - - ```python - '''Pass the required SPN values directly into the credential object, does not require AZ PowerShell or AZ CLI''' + import os from pathlib import Path - - from azure.identity import ClientSecretCredential + from azure.identity import ManagedIdentityCredential from fabric_cicd import FabricWorkspace, publish_all_items, unpublish_all_orphan_items - # Assumes your script is one level down from root - root_directory = Path(__file__).resolve().parent + # GitHub Actions sets GITHUB_WORKSPACE automatically + root_directory = Path(os.getenv("GITHUB_WORKSPACE", ".")).resolve() # Sample values for FabricWorkspace parameters - workspace_id = "your-workspace-id" - environment = "your-environment" + workspace_id = os.getenv("WORKSPACE_ID") + environment = os.getenv("ENVIRONMENT", "PROD") repository_directory = str(root_directory / "your-workspace-directory") item_type_in_scope = ["Notebook", "DataPipeline", "Environment"] - # Use Azure CLI credential to authenticate - client_id = "your-client-id" - client_secret = "your-client-secret" - tenant_id = "your-tenant-id" - token_credential = ClientSecretCredential(client_id=client_id, client_secret=client_secret, tenant_id=tenant_id) + # Use system-assigned managed identity + token_credential = ManagedIdentityCredential() # Initialize the FabricWorkspace object with the required parameters target_workspace = FabricWorkspace( @@ -395,71 +373,137 @@ This approach utilizes directly passing in SPN Client Id and Client Secret. Alth unpublish_all_orphan_items(target_workspace) ``` -=== "Azure DevOps" +## Fabric Notebook Authentication + +When running fabric-cicd within Microsoft Fabric Notebooks, authentication is handled automatically through the user session context. No explicit `token_credential` parameter is required. Alternatively, if you want to use a different identity than the logged-in user, you can override the automatic authentication by providing an explicit credential. + +=== "No Credential" ```python ''' - Pass the required SPN values directly into the credential object - OR Store the SPN Secret in Key Vault and reference key vault in Python - OR Store the SPN Secret in Key Vault, link key vault to ADO variable group, and reference variable group environment variable in Python - OR (Preferred) Use AZ CLI or AZ PowerShell task and leverage Service Connection (defined above) - + fabric-cicd automatically uses the Fabric Notebook session authentication + Most common pattern: clone repository and deploy from within notebook ''' - import sys + import tempfile + import subprocess import os - from pathlib import Path + from fabric_cicd import FabricWorkspace, publish_all_items, unpublish_all_orphan_items - from azure.identity import ClientSecretCredential - from fabric_cicd import FabricWorkspace, publish_all_items, unpublish_all_orphan_items, change_log_level + # Sample configuration values + workspace_id = "your-workspace-id" + environment = "your-environment" + repo_url = "https://github.com/your-org/your-repo.git" + repo_ref = "main" + workspace_directory = "your-workspace-directory" - # Force unbuffered output like `python -u` - sys.stdout.reconfigure(line_buffering=True, write_through=True) - sys.stderr.reconfigure(line_buffering=True, write_through=True) + # Use context manager for automatic cleanup (even on exceptions) + with tempfile.TemporaryDirectory(prefix="cloned_repo_") as temp_dir: + print(f"Created temporary directory: {temp_dir}") - # Enable debugging if defined in Azure DevOps pipeline - if os.getenv("SYSTEM_DEBUG", "false").lower() == "true": - change_log_level("DEBUG") + # Clone the repository + print(f"Cloning {repo_url} (ref: {repo_ref})...") + result = subprocess.run( + ["git", "clone", "--branch", repo_ref, "--single-branch", repo_url, temp_dir], + capture_output=True, + text=True + ) - # Assumes your script is one level down from root - root_directory = Path(__file__).resolve().parent + if result.returncode != 0: + raise Exception(f"Git clone failed: {result.stderr}") - # Sample values for FabricWorkspace parameters - workspace_id = "your-workspace-id" - environment = "your-environment" - repository_directory = str(root_directory / "your-workspace-directory") - item_type_in_scope = ["Notebook", "DataPipeline", "Environment"] + workspace_root = os.path.join(temp_dir, workspace_directory) - # Use Azure CLI credential to authenticate - client_id = "your-client-id" - client_secret = "your-client-secret" - tenant_id = "your-tenant-id" - token_credential = ClientSecretCredential(client_id=client_id, client_secret=client_secret, tenant_id=tenant_id) + # Deploy workspace items from cloned repository + item_type_in_scope = ["Notebook", "DataPipeline", "Environment"] - # Initialize the FabricWorkspace object with the required parameters - target_workspace = FabricWorkspace( - workspace_id=workspace_id, - environment=environment, - repository_directory=repository_directory, - item_type_in_scope=item_type_in_scope, - token_credential=token_credential, - ) + # Initialize FabricWorkspace - no token_credential needed + target_workspace = FabricWorkspace( + workspace_id=workspace_id, + environment=environment, + repository_directory=workspace_root, + item_type_in_scope=item_type_in_scope + ) - # Publish all items defined in item_type_in_scope - publish_all_items(target_workspace) + # Publish all items defined in item_type_in_scope + publish_all_items(target_workspace) - # Unpublish all items defined in item_type_in_scope not found in repository - unpublish_all_orphan_items(target_workspace) + # Unpublish all items defined in item_type_in_scope not found in repository + unpublish_all_orphan_items(target_workspace) + + # Directory automatically cleaned up here + print("Cleaned up temporary directory") ``` -=== "GitHub" +=== "Credential" ```python - '''Unconfirmed example at this time, however, the Azure DevOps example is a good starting point''' - ``` + ''' + Override automatic authentication with explicit credential + Only needed for specific identity requirements + ''' -=== "Fabric Notebook" + import tempfile + import subprocess + import os + from azure.identity import ClientSecretCredential + from fabric_cicd import FabricWorkspace, publish_all_items, unpublish_all_orphan_items - ```python - '''Unconfirmed example at this time, Default Credential flow is recommended''' + # Sample configuration values + workspace_id = "your-workspace-id" + environment = "your-environment" + repo_url = "https://github.com/your-org/your-repo.git" + repo_ref = "main" + workspace_directory = "your-workspace-directory" + + # Use explicit SPN auth (overrides automatic authentication) + # Retrieve secrets from Azure Key Vault using notebookutils + key_vault_url = "https://your-keyvault.vault.azure.net/" + client_id = notebookutils.credentials.getSecret(key_vault_url, "client-id") + client_secret = notebookutils.credentials.getSecret(key_vault_url, "client-secret") + tenant_id = notebookutils.credentials.getSecret(key_vault_url, "tenant-id") + + token_credential = ClientSecretCredential( + client_id=client_id, + client_secret=client_secret, + tenant_id=tenant_id + ) + + # Use context manager for automatic cleanup (even on exceptions) + with tempfile.TemporaryDirectory(prefix="cloned_repo_") as temp_dir: + print(f"Created temporary directory: {temp_dir}") + + # Clone the repository + print(f"Cloning {repo_url} (ref: {repo_ref})...") + result = subprocess.run( + ["git", "clone", "--branch", repo_ref, "--single-branch", repo_url, temp_dir], + capture_output=True, + text=True + ) + + if result.returncode != 0: + raise Exception(f"Git clone failed: {result.stderr}") + + workspace_root = os.path.join(temp_dir, workspace_directory) + + # Deploy workspace items from cloned repository + item_type_in_scope = ["Notebook", "DataPipeline", "Environment"] + + # Initialize with explicit credential + target_workspace = FabricWorkspace( + workspace_id=workspace_id, + environment=environment, + repository_directory=workspace_root, + item_type_in_scope=item_type_in_scope, + token_credential=token_credential, + ) + + # Publish all items defined in item_type_in_scope + publish_all_items(target_workspace) + + # Unpublish all items defined in item_type_in_scope not found in repository + unpublish_all_orphan_items(target_workspace) + + # Directory automatically cleaned up here + print("Cleaned up temporary directory") ``` diff --git a/docs/example/deployment_variable.md b/docs/example/deployment_variable.md index c950ba07..c42d7364 100644 --- a/docs/example/deployment_variable.md +++ b/docs/example/deployment_variable.md @@ -1,6 +1,10 @@ # Deployment Variable Examples -A key concept in CI/CD is defining environment-specific deployment variables. The following are examples on how to inject variables from outside of the python script to handle values that are environment specific, or common accross other tooling. +A key concept in CI/CD is defining environment-specific deployment variables. The following are examples of how to inject variables from outside of the python script to handle values that are environment-specific, or common across other tooling. These examples provide starting points that should be adapted for your specific environment and security requirements. + +> **⚠️ DEPRECATION NOTICE:** Due to security best practices, the **Default Credential** (`DefaultAzureCredential`) authentication method is deprecated and will be removed in a future release. All examples below use explicit credential methods. + +**Note:** All examples below use the `AzureCliCredential` token for demonstration purposes. You can substitute this with other explicit credential methods based on your environment. ## Branch Based @@ -9,13 +13,16 @@ Leverage the following when you have specific values that you need to define per === "Local" ```python - '''Leverages Default Credential Flow for authentication. Determines variables based on locally checked out branch.''' + '''Determines variables based on locally checked out branch''' from pathlib import Path - import git # Depends on pip install gitpython + from azure.identity import AzureCliCredential from fabric_cicd import FabricWorkspace, publish_all_items, unpublish_all_orphan_items + # Use Azure CLI credential to authenticate + token_credential = AzureCliCredential() + # Assumes your script is one level down from root root_directory = Path(__file__).resolve().parent @@ -43,6 +50,7 @@ Leverage the following when you have specific values that you need to define per environment=environment, repository_directory=repository_directory, item_type_in_scope=item_type_in_scope, + token_credential=token_credential, ) # Publish all items defined in item_type_in_scope @@ -50,18 +58,20 @@ Leverage the following when you have specific values that you need to define per # Unpublish all items defined in item_type_in_scope not found in repository unpublish_all_orphan_items(target_workspace) - ``` === "Azure DevOps" ```python - '''Leverages Default Credential Flow for authentication. Determines variables based on the branch that originated the build.''' + ''' + Determines variables based on the branch that originated the build + Uses Azure CLI credential with service connection + ''' import sys import os from pathlib import Path - + from azure.identity import AzureCliCredential from fabric_cicd import FabricWorkspace, publish_all_items, unpublish_all_orphan_items, change_log_level # Force unbuffered output like `python -u` @@ -72,6 +82,9 @@ Leverage the following when you have specific values that you need to define per if os.getenv("SYSTEM_DEBUG", "false").lower() == "true": change_log_level("DEBUG") + # Use Azure CLI credential to authenticate + token_credential = AzureCliCredential() + # Assumes your script is one level down from root root_directory = Path(__file__).resolve().parent @@ -91,13 +104,13 @@ Leverage the following when you have specific values that you need to define per repository_directory = str(root_directory / "your-workspace-directory") item_type_in_scope = ["Notebook", "DataPipeline", "Environment"] - # Initialize the FabricWorkspace object with the required parameters target_workspace = FabricWorkspace( workspace_id=workspace_id, environment=environment, repository_directory=repository_directory, item_type_in_scope=item_type_in_scope, + token_credential=token_credential, ) # Publish all items defined in item_type_in_scope @@ -110,7 +123,53 @@ Leverage the following when you have specific values that you need to define per === "GitHub" ```python - '''Unconfirmed example at this time, however, the Azure DevOps example is a good starting point''' + ''' + Determines variables based on the branch that originated the build + Uses Azure CLI credential (requires az login in GitHub Actions workflow) + ''' + + import os + from pathlib import Path + from azure.identity import AzureCliCredential + from fabric_cicd import FabricWorkspace, publish_all_items, unpublish_all_orphan_items + + # Use Azure CLI credential to authenticate + token_credential = AzureCliCredential() + + # GitHub Actions sets GITHUB_WORKSPACE automatically + root_directory = Path(os.getenv("GITHUB_WORKSPACE", ".")).resolve() + + # Get branch from GitHub environment variable + branch = os.getenv("GITHUB_REF_NAME") + + # The defined environment values should match the names found in the parameter.yml file + if branch == "dev": + workspace_id = "dev-workspace-id" + environment = "DEV" + elif branch == "main": + workspace_id = "prod-workspace-id" + environment = "PROD" + else: + raise ValueError("Invalid branch to deploy from") + + # Sample values for FabricWorkspace parameters + repository_directory = str(root_directory / "your-workspace-directory") + item_type_in_scope = ["Notebook", "DataPipeline", "Environment"] + + # Initialize the FabricWorkspace object with the required parameters + target_workspace = FabricWorkspace( + workspace_id=workspace_id, + environment=environment, + repository_directory=repository_directory, + item_type_in_scope=item_type_in_scope, + token_credential=token_credential, + ) + + # Publish all items defined in item_type_in_scope + publish_all_items(target_workspace) + + # Unpublish all items defined in item_type_in_scope not found in repository + unpublish_all_orphan_items(target_workspace) ``` ## Passed Arguments @@ -120,17 +179,20 @@ Leverage the following when you want to pass in variables outside of the python === "Local" ```python - '''Leverages Default Credential Flow for authentication. Accepts parameters passed into Python during execution.''' + '''Accepts parameters passed into Python during execution''' import argparse - + from azure.identity import AzureCliCredential from fabric_cicd import FabricWorkspace, publish_all_items, unpublish_all_orphan_items + # Use Azure CLI credential to authenticate + token_credential = AzureCliCredential() + # Accept parsed arguments - parser = argparse.ArgumentParser(description='Process Azure Pipeline arguments.') - parser.add_argument('--workspace_id', type=str) + parser = argparse.ArgumentParser(description='Process deployment arguments.') + parser.add_argument('--workspace_id', type=str, required=True) parser.add_argument('--environment', type=str) - parser.add_argument('--repository_directory', type=str) + parser.add_argument('--repository_directory', type=str, required=True) parser.add_argument('--items_in_scope', type=str) args = parser.parse_args() @@ -138,7 +200,7 @@ Leverage the following when you want to pass in variables outside of the python workspace_id = args.workspace_id environment = args.environment repository_directory = args.repository_directory - item_type_in_scope = args.items_in_scope.split(",") + item_type_in_scope = args.items_in_scope.split(",") if args.items_in_scope else ["Notebook", "DataPipeline", "Environment"] # Initialize the FabricWorkspace object with the required parameters target_workspace = FabricWorkspace( @@ -146,6 +208,7 @@ Leverage the following when you want to pass in variables outside of the python environment=environment, repository_directory=repository_directory, item_type_in_scope=item_type_in_scope, + token_credential=token_credential, ) # Publish all items defined in item_type_in_scope @@ -153,18 +216,21 @@ Leverage the following when you want to pass in variables outside of the python # Unpublish all items defined in item_type_in_scope not found in repository unpublish_all_orphan_items(target_workspace) - ``` === "Azure DevOps" ```python - '''Leverages Default Credential Flow for authentication. Accepts parameters passed into Python during execution.''' + ''' + Accepts parameters passed into Python during execution + Uses Azure CLI credential with service connection + ''' import sys import os + import argparse from pathlib import Path - + from azure.identity import AzureCliCredential from fabric_cicd import FabricWorkspace, publish_all_items, unpublish_all_orphan_items, change_log_level # Force unbuffered output like `python -u` @@ -175,12 +241,15 @@ Leverage the following when you want to pass in variables outside of the python if os.getenv("SYSTEM_DEBUG", "false").lower() == "true": change_log_level("DEBUG") + # Use Azure CLI credential to authenticate + token_credential = AzureCliCredential() + # Accept parsed arguments - parser = argparse.ArgumentParser(description='Process Azure Pipeline arguments.') - parser.add_argument('--workspace_id', type=str) - parser.add_argument('--environment', type=str) - parser.add_argument('--repository_directory', type=str) - parser.add_argument('--items_in_scope', type=str) + parser = argparse.ArgumentParser(description='Process deployment arguments.') + parser.add_argument('--workspace_id', type=str, required=True) + parser.add_argument('--environment', type=str, required=True) + parser.add_argument('--repository_directory', type=str, required=True) + parser.add_argument('--items_in_scope', type=str, required=True) args = parser.parse_args() # Sample values for FabricWorkspace parameters @@ -195,6 +264,7 @@ Leverage the following when you want to pass in variables outside of the python environment=environment, repository_directory=repository_directory, item_type_in_scope=item_type_in_scope, + token_credential=token_credential, ) # Publish all items defined in item_type_in_scope diff --git a/docs/example/release_pipeline.md b/docs/example/release_pipeline.md index 21156851..56a28ccc 100644 --- a/docs/example/release_pipeline.md +++ b/docs/example/release_pipeline.md @@ -4,7 +4,7 @@ The following are some common examples of how to deploy from tooling like Azure ## Azure CLI -This approach will work for both the Default Credential Flow and the Azure CLI Credential Flow. However, it is recommended to use the Azure CLI Credential Flow in case there are multiple identities present in the build VM. +This approach uses the Azure CLI Credential Flow. An explicit credential method is required. This avoids ambiguity when multiple identities are present in the build VM. === "Azure DevOps" @@ -42,12 +42,12 @@ This approach will work for both the Default Credential Flow and the Azure CLI C === "GitHub" ```yaml - ###Unconfirmed example at this time, however, the Azure DevOps example is a good starting point + # Unconfirmed example at this time. The Azure DevOps example is a good starting point. ``` ## Azure PowerShell -This approach will work for both the Default Credential Flow and the Azure PowerShell Credential Flow. However, it is recommended to use the Azure PowerShell Credential Flow in case there are multiple identities present in the build VM. +This approach uses the Azure PowerShell Credential Flow. An explicit credential method is required. This avoids ambiguity when multiple identities are present in the build VM. === "Azure DevOps" @@ -86,12 +86,12 @@ This approach will work for both the Default Credential Flow and the Azure Power === "GitHub" ```yaml - ###Unconfirmed example at this time, however, the Azure DevOps example is a good starting point + # Unconfirmed example at this time. The Azure DevOps example is a good starting point. ``` ## Variable Groups -This approach is best suited for the Passed Arguments example found in the Deployment Variable Examples, in combination with the Explicit SPN Credential flow in the Authentication Examples. The goal being to define values within the pipeline (or outside the pipeline in Azure DevOps variable groups) and inject them into the python script. Note this also doesn't take a dependency on PowerShell for those organizations or scenarios that PowerShell is not allowed. +This approach is best suited for the Passed Arguments example found in the Deployment Variable Examples, in combination with a `ClientSecretCredential` as shown in the [Authentication Examples](authentication.md). The goal is to define values within the pipeline (or outside the pipeline in Azure DevOps variable groups) and inject them into the python script. Note this also doesn't take a dependency on PowerShell for those organizations or scenarios where PowerShell is not allowed. === "Azure DevOps" ```yml @@ -136,12 +136,12 @@ This approach is best suited for the Passed Arguments example found in the Deplo --tenant_id $(tenant_id) # from Fabric_Deployment_Group_KeyVault --workspace_id $(workspace_id) # from Fabric_Deployment_Group --environment $(environment_name) # from Fabric_Deployment_Group - --repository_directory $repository_directory # from Fabric_Deployment_Group + --repository_directory $(repository_directory) # from Fabric_Deployment_Group --item_types_in_scope ${{ parameters.items_in_scope }} ``` === "GitHub" ```yaml - ###Unconfirmed example at this time, however, the Azure DevOps example is a good starting point + # Unconfirmed example at this time. The Azure DevOps example is a good starting point. ``` diff --git a/docs/how_to/getting_started.md b/docs/how_to/getting_started.md index 558389a3..b3c383d2 100644 --- a/docs/how_to/getting_started.md +++ b/docs/how_to/getting_started.md @@ -10,13 +10,34 @@ pip install fabric-cicd ## Authentication -- You can optionally provide your own credential object that aligns with the `TokenCredential` class. For more details, see the [TokenCredential](https://learn.microsoft.com/en-us/dotnet/api/azure.core.tokencredential) documentation. -- If you do not provide a `token_credential` parameter, the library will use the Azure SDK's `DefaultAzureCredential` for authentication. +> **⚠️ DEPRECATION NOTICE**: The `DefaultAzureCredential` fallback is deprecated and will be removed in a future release. Please provide an explicit `token_credential` parameter. - - Refer to the [Azure SDK](https://learn.microsoft.com/en-us/azure/developer/python/sdk/authentication/credential-chains?tabs=dac#defaultazurecredential-overview) documentation for the order in which credential types are attempted. - - For local development with a User Principal Name (UPN), install either the [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli-windows) or the [Az.Accounts](https://www.powershellgallery.com/packages/Az.Accounts/2.2.3) PowerShell module. +- You must provide your own credential object that aligns with the `TokenCredential` class (from [azure.identity](https://learn.microsoft.com/en-us/python/api/azure-identity/azure.identity?view=azure-python)). For more details, see the [TokenCredential](https://learn.microsoft.com/en-us/python/api/azure-core/azure.core.credentials.tokencredential?view=azure-python) documentation. +- When running in Fabric Notebook runtime, authentication is handled automatically through the user session context and therefore an explicit credential is not required in this scenario. - - Note: When no credential is provided, the `DefaultAzureCredential` may select an unexpected identity. For example, if you log in to the Azure CLI with a Service Principal Name (SPN) but log in to Az.Accounts with a UPN, the `DefaultAzureCredential` will prioritize the CLI authentication. +**Recommended Authentication Methods:** + +- For local development: `AzureCliCredential` or `AzurePowerShellCredential` (user authentication) +- For CI/CD pipelines: `AzureCliCredential`/`AzurePowerShellCredential` (platform authentication), `ClientSecretCredential` (service principal), or `ManagedIdentityCredential` (self-hosted agents) + +**Basic Example:** + +```python +from azure.identity import AzureCliCredential +from fabric_cicd import FabricWorkspace + +token_credential = AzureCliCredential() + +workspace = FabricWorkspace( + workspace_id="your-workspace-id", + environment="your-target-environment", + repository_directory="your-repository-directory", + item_type_in_scope=["Notebook", "DataPipeline", "Environment"], + token_credential=token_credential, +) +``` + +See the [Authentication Examples](../example/authentication.md) for specific implementation patterns. ## Directory Structure @@ -40,10 +61,10 @@ This library deploys from a directory containing files and directories committed The flow pictured below is the hero scenario for this library and is the recommendation if you're just starting out. -- `Deployed` branches are not connected to workspaces via [GIT Sync](https://learn.microsoft.com/en-us/fabric/cicd/git-integration/git-get-started?tabs=azure-devops%2CAzure%2Ccommit-to-git#connect-a-workspace-to-a-git-repo) -- `Feature` branches are connected to workspaces via [GIT Sync](https://learn.microsoft.com/en-us/fabric/cicd/git-integration/git-get-started?tabs=azure-devops%2CAzure%2Ccommit-to-git#connect-a-workspace-to-a-git-repo) -- `Deployed` workspaces are only updated through script-based deployments, such as through the fabric-cicd library -- `Feature` branches are created from the default branch, merged back into the default `Deployed` branch, and cherry picked into the upper `Deployed` branches -- Each deployment is a full deployment and does not consider commit diffs +- `Deployed` branches are not connected to workspaces via [GIT Sync](https://learn.microsoft.com/en-us/fabric/cicd/git-integration/git-get-started?tabs=azure-devops%2CAzure%2Ccommit-to-git#connect-a-workspace-to-a-git-repo) +- `Feature` branches are connected to workspaces via [GIT Sync](https://learn.microsoft.com/en-us/fabric/cicd/git-integration/git-get-started?tabs=azure-devops%2CAzure%2Ccommit-to-git#connect-a-workspace-to-a-git-repo) +- `Deployed` workspaces are only updated through script-based deployments, such as through the fabric-cicd library +- `Feature` branches are created from the default branch, merged back into the default `Deployed` branch, and cherry picked into the upper `Deployed` branches +- Each deployment is a full deployment and does not consider commit diffs ![GIT Flow](../config/assets/git_flow.png) diff --git a/docs/how_to/troubleshooting.md b/docs/how_to/troubleshooting.md index 3a875d71..f5864356 100644 --- a/docs/how_to/troubleshooting.md +++ b/docs/how_to/troubleshooting.md @@ -10,7 +10,7 @@ fabric-cicd includes a debug logging feature that provides detailed visibility i **Default Behavior:** -- Without debug logging enabled, fabric-cicd displays only high-level progress messages, warnings and errors +- Without debug logging enabled, fabric-cicd displays only high-level progress messages, warnings, and errors - The `fabric_cicd.error.log` file will contain the same lines printed to the console along with stack traces for any errors **Enabling Debug Logging:** @@ -129,7 +129,14 @@ Traceback (most recent call last): **Solution**: -1. Verify you're logged in with Azure CLI or Az.Accounts PowerShell module: +1. Explicit authentication is **strongly recommended** as `DefaultAzureCredential` fallback is deprecated and will be removed. Choose the appropriate method for your scenario: + - Local development: `AzureCliCredential` (requires `az login`) or `AzurePowerShellCredential` (requires `Connect-AzAccount`) + - CI/CD pipelines with platform auth: `AzureCliCredential` or `AzurePowerShellCredential` (requires a prior login step in the workflow, e.g., `azure/login` or AzCLI task) + - CI/CD pipelines with service principals: `ClientSecretCredential` (requires client ID, secret, and tenant ID) + - CI/CD pipelines with managed identity: `ManagedIdentityCredential` (requires Azure-hosted self-hosted runners) + - Fabric Notebooks: Authentication is handled automatically within the Fabric runtime (no explicit credential required unless overriding with a specific identity) + +2. Verify authentication setup: ```bash az login ``` @@ -137,8 +144,11 @@ Traceback (most recent call last): ```powershell Connect-AzAccount ``` -2. Check that your account has appropriate permissions on the target workspace -3. If using Service Principal authentication, verify client ID, secret, and tenant ID are correct +3. Check permissions: ensure your account has appropriate permissions on the target workspace + +4. For Service Principal authentication: verify client ID, secret, and tenant ID are correct + +5. See detailed examples: refer to [authentication examples](../example/authentication.md) for platform-specific implementation guidance #### Item Deployment Failures @@ -184,13 +194,13 @@ The `devtools/` directory contains pre-built scripts to help test and validate d **Key Configuration Options**: -| Configuration | Description | Required | -| ---------------------- | ------------------------------------------------------------------ | -------- | -| `workspace_id` | Target Fabric workspace ID | Yes | -| `environment` | Target environment (used for parameterization) | No | -| `repository_directory` | Path to Fabric workspace items files (absolute or relative path) | Yes | -| `item_type_in_scope` | Specific item types to deploy (defaults to all supported types) | No | -| `token_credential` | Service Principal credentials (defaults to DefaultAzureCredential) | No | +| Configuration | Description | Required | +| ---------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- | +| `workspace_id` | Target Fabric workspace ID | Yes | +| `environment` | Target environment (used for parameterization) | No | +| `repository_directory` | Path to Fabric workspace items files (absolute or relative path) | Yes | +| `item_type_in_scope` | Specific item types to deploy (defaults to all supported types) | No | +| `token_credential` | Explicit credential method (`AzureCliCredential`, etc.) | No (default credential fallback is deprecated; will be required) | **Quick Start**: @@ -216,7 +226,17 @@ repository_directory = "sample/workspace" # Deploy only specific item types item_type_in_scope = ["Environment", "Notebook", "DataPipeline"] -# Use Service Principal authentication +# Authentication examples - choose one: + +# For local development with Azure CLI +from azure.identity import AzureCliCredential +token_credential = AzureCliCredential() + +# For local development with Azure PowerShell +from azure.identity import AzurePowerShellCredential +token_credential = AzurePowerShellCredential() + +# For CI/CD with Service Principal from azure.identity import ClientSecretCredential token_credential = ClientSecretCredential( client_id="your-client-id", @@ -224,6 +244,10 @@ token_credential = ClientSecretCredential( tenant_id="your-tenant-id" ) +# For Azure-hosted runners with Managed Identity +from azure.identity import ManagedIdentityCredential +token_credential = ManagedIdentityCredential() + # Override constant value constants.DEFAULT_API_ROOT_URL = "https://api.fabric.microsoft.com" ``` @@ -234,12 +258,12 @@ constants.DEFAULT_API_ROOT_URL = "https://api.fabric.microsoft.com" **Key Configuration Options**: -| Configuration | Description | Required | -| ------------------ | ----------------------------------------------------------------------------------- | -------- | -| `config_file` | Path to your `config.yml` file | Yes | -| `environment` | Target environment (used for parameterization and environment-based configurations) | No | -| `token_credential` | Service Principal credentials (defaults to DefaultAzureCredential) | No | -| `config_override` | Dictionary to override configuration values within `config.yml` | No | +| Configuration | Description | Required | +| ------------------ | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------- | +| `config_file` | Path to your `config.yml` file | Yes | +| `environment` | Target environment (used for parameterization and environment-based configurations) | No | +| `token_credential` | Explicit credential method (`AzureCliCredential`, etc.) | No (default credential fallback is deprecated; will be required) | +| `config_override` | Dictionary to override configuration values within `config.yml` | No | **Quick Start**: @@ -285,7 +309,7 @@ See [parameterization](parameterization.md#parameter-file-validation) for more i | `api_url` | Full API endpoint URL | Yes | | `method` | HTTP method (GET, POST, DELETE, PATCH) | Yes | | `body` | Request payload (for POST/PATCH) | Varies | -| `token_credential` | Service Principal credentials (defaults to DefaultAzureCredential) | No | +| `token_credential` | Explicit credential method (`AzureCliCredential`, etc.) | Yes | | other | View `invoke()` in `FabricEndpoint` class for additional parameters | No | **Quick Start**: @@ -318,6 +342,7 @@ If you're still experiencing issues after following this guide: ## Additional Resources +- [Authentication Examples](../example/authentication.md) - Comprehensive authentication implementation examples - [Contribution Guide](https://github.com/microsoft/fabric-cicd/blob/main/CONTRIBUTING.md) - Setup instructions and PR requirements - [Feature Flags](optional_feature.md#feature-flags) - Available feature flags for advanced scenarios - [Getting Started](getting_started.md) - Basic installation and authentication diff --git a/src/fabric_cicd/fabric_workspace.py b/src/fabric_cicd/fabric_workspace.py index b7bd152f..88b5c78c 100644 --- a/src/fabric_cicd/fabric_workspace.py +++ b/src/fabric_cicd/fabric_workspace.py @@ -104,7 +104,11 @@ def __init__( if _is_fabric_runtime(): token_credential = _generate_fabric_credential() else: - # if credential is not defined, use DefaultAzureCredential + logger.warning( + "DeprecationWarning: DefaultAzureCredential fallback is deprecated and will be removed in a future release. " + "Please pass the token_credential explicitly (e.g., AzureCliCredential(), etc.)" + ) + # TODO: Remove DefaultAzureCredential fallback logic from azure.identity import DefaultAzureCredential token_credential = DefaultAzureCredential()