Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
b32c344
Introduce deploy command
Feb 19, 2026
a5f3d49
add changie
Feb 19, 2026
c319fb6
Resolve PR commants
Feb 19, 2026
a727e79
Create MSAL bridge
Feb 22, 2026
3c3bf67
remove metavar
Feb 22, 2026
c472fac
align context with other commands - will handle it in another PR
Feb 22, 2026
176c090
Merge pull request #2 from aviatco/deploy-command-interface
aviatco Feb 22, 2026
f9cb1f7
Merge branch 'dev/aviatcohen/cli-cicd-intergation-main' into dv/aviat…
Feb 22, 2026
1a9bd67
call cicd deploy_with_config
Feb 22, 2026
4c53e4e
resolve PR cooments
Feb 24, 2026
508b7ca
Update src/fabric_cli/core/fab_auth.py
aviatco Feb 26, 2026
06c2dd8
resolve commants
Mar 1, 2026
d844bba
remove handling of expires_on when converting msal token to azure ide…
Mar 1, 2026
170b45a
make sure expires_on is int
Mar 1, 2026
a02687e
Merge pull request #3 from aviatco/dev/aviatcohen/msal-bridge
aviatco Mar 1, 2026
5972eb1
support deploy command
Mar 1, 2026
73bbdbd
Merge branch 'dev/aviatcohen/msal-bridge' into dev/aviatcohen/deploy-…
Mar 1, 2026
e9c0fc0
using expires_in if expires_on is not avilable
Mar 2, 2026
d1de4ec
Merge branch 'dev/aviatcohen/cli-cicd-intergation-main' into dev/avia…
Mar 2, 2026
870e923
comment to disable live recording mode
Mar 2, 2026
4fb7b7b
change --deploy_with_config flag to config; fix PR comments
Mar 3, 2026
3d733cd
create deploy_setup fixture and update tests
Mar 4, 2026
08d7d8d
add tests
Mar 5, 2026
9991a00
Update src/fabric_cli/commands/fs/deploy/fab_fs_deploy_config_file.py
aviatco Mar 5, 2026
d3e8dba
update cicd feature flags and debug_enabled
Mar 5, 2026
69ef52c
Merge branch 'dev/aviatcohen/deploy-with-config' of https://github.co…
Mar 5, 2026
d21ff0d
update cicd feature flags and debug_enabled
Mar 5, 2026
ca31a7b
Merge pull request #4 from aviatco/dev/aviatcohen/deploy-with-config
aviatco Mar 5, 2026
a2a3243
add changie
Mar 5, 2026
785ee13
align seccess msg with cicd updates
Mar 5, 2026
58acead
define VALID_DEFATULT_SCOPES at module level
Mar 5, 2026
a4c8c96
Merge branch 'main' into dev/aviatcohen/cli-cicd-intergation-main
aviatco Mar 5, 2026
c13be3d
Fix type check tests
Mar 5, 2026
1511aba
fix type check error for expires_on
Mar 5, 2026
de8660f
remove python 3.13 from test build just for testing - will be revertg…
Mar 5, 2026
8d55167
resolve backslash issue in prompt message
Mar 5, 2026
a764267
fix test_get_access_token_token_error
Mar 5, 2026
128702c
Apply suggestion from @aviatco
aviatco Mar 5, 2026
75c35f1
trying to fix interactive_cli tests
Mar 5, 2026
264bdbc
Merge branch 'dev/aviatcohen/cli-cicd-intergation-main' of https://gi…
Mar 5, 2026
39aa30e
deploy documentation
Mar 9, 2026
e6cf9d5
Merge branch 'main' into dev/aviatcohen/cli-cicd-intergation-main
aviatco Mar 9, 2026
9467b6b
bump cicd version
Mar 9, 2026
d4f5f52
remove deploy docs
Mar 9, 2026
c1082b3
Merge branch 'dev/aviatcohen/cli-cicd-intergation-main' of https://gi…
Mar 9, 2026
1b8b174
revert - remove python 3.13 support
Mar 9, 2026
64838f0
fix deploy tests
Mar 9, 2026
b7bdfde
mock acquire_token method
Mar 9, 2026
0a8def4
mock acquire_token
Mar 10, 2026
7fb1e55
new decording for test_deploy_single_item_success
Mar 10, 2026
dc7fd75
revert scope class fixture
Mar 10, 2026
23f06f1
revert setup_default_format scopr class in mock_fab_set_state_config
Mar 10, 2026
e4cf5b2
revert setup_default_format scope class in setup_default_format
Mar 10, 2026
17e4a67
recorde fail tests
Mar 10, 2026
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
6 changes: 6 additions & 0 deletions .changes/unreleased/added-20260305-091728.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: added
body: Introduces the new deploy command that integrates with the fabric-cicd library, enabling users to deploy multiple Fabric items in a single
time: 2026-03-05T09:17:28.270036405Z
custom:
Author: aviatco
AuthorLink: https://github.com/aviatco
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ dependencies = [
"msal[broker]>=1.34,<2 ; platform_system != 'Linux'",
"msal>=1.34,<2",
"msal_extensions",
"azure-core>=1.29.0",
"questionary",
"prompt_toolkit>=3.0.41",
"cachetools>=5.5.0",
Expand All @@ -28,6 +29,7 @@ dependencies = [
"psutil==7.0.0",
"requests",
"cryptography",
"fabric-cicd>=0.3.0",
]

[project.scripts]
Expand Down
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ argcomplete>=3.6.2
psutil==7.0.0
requests
cryptography
fabric-cicd>=0.3.0

# Testing and Building Requirements
tox>=4.20.0
Expand Down
56 changes: 56 additions & 0 deletions src/fabric_cli/commands/fs/deploy/fab_fs_deploy_config_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

import json
from argparse import Namespace

from fabric_cicd import append_feature_flag, configure_external_file_logging, deploy_with_config, disable_file_logging # type: ignore

from fabric_cli.core import fab_constant, fab_state_config
from fabric_cli.core import fab_logger
from fabric_cli.core.fab_exceptions import FabricCLIError
from fabric_cli.core.fab_msal_bridge import create_fabric_token_credential
from fabric_cli.utils import fab_ui
from fabric_cli.utils.fab_util import get_dict_from_params


def deploy_with_config_file(args: Namespace) -> None:
"""deploy fabric items to a workspace using a configuration file and target environment - delegates to CICD library."""

try:
if fab_state_config.get_config(fab_constant.FAB_DEBUG_ENABLED) == "true":
cli_logger = fab_logger.get_logger()
# configure file logging for CICD library to use the same file handler as the CLI
configure_external_file_logging(cli_logger)
else:
# prevent creation of a log file for fabric-cicd logs when debug mode is disabled
disable_file_logging()

# feature flags to avoid printing identity info in logs
append_feature_flag("disable_print_identity")

deploy_config_file = args.config
deploy_parameters = get_dict_from_params(args.params, max_depth=1)
for param in deploy_parameters:
if isinstance(deploy_parameters[param], str):
try:
deploy_parameters[param] = json.loads(
deploy_parameters[param])
except json.JSONDecodeError:
# If it's not a valid JSON string, keep it as is
pass
result = deploy_with_config(
config_file_path=deploy_config_file,
environment=args.target_env,
token_credential=create_fabric_token_credential(), # MSAL bridge TokenCredential
**deploy_parameters
)

if result:
fab_ui.print_output_format(
args, message=result.message)

except Exception as e:
raise FabricCLIError(
f"Deployment failed: {str(e)}",
fab_constant.ERROR_IN_DEPLOYMENT)
7 changes: 7 additions & 0 deletions src/fabric_cli/commands/fs/fab_fs.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from fabric_cli.commands.fs import fab_fs_export as fs_export
from fabric_cli.commands.fs import fab_fs_get as fs_get
from fabric_cli.commands.fs import fab_fs_import as fs_import
from fabric_cli.commands.fs import fab_fs_deploy as fs_deploy
from fabric_cli.commands.fs import fab_fs_ln as fs_ln
from fabric_cli.commands.fs import fab_fs_ls as fs_ls
from fabric_cli.commands.fs import fab_fs_mkdir as fs_mkdir
Expand Down Expand Up @@ -144,6 +145,12 @@ def import_command(args: Namespace) -> None:
fs_import.exec_command(args, context)


@handle_exceptions()
@set_command_context()
def deploy_command(args: Namespace) -> None:
fs_deploy.exec_command(args)


@handle_exceptions()
@set_command_context()
def set_command(args: Namespace) -> None:
Expand Down
14 changes: 14 additions & 0 deletions src/fabric_cli/commands/fs/fab_fs_deploy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

from argparse import Namespace

from fabric_cli.commands.fs.deploy.fab_fs_deploy_config_file import deploy_with_config_file
from fabric_cli.utils import fab_ui


def exec_command(args: Namespace) -> None:
"""deploy fabric items to a workspace using a configuration file and target environment - CICD flow."""
target_env_msg = 'without a target environment' if args.target_env == 'N/A' else f"to target environment '{args.target_env}'"
if args.force or fab_ui.prompt_confirm(f"Are you sure you want to deploy {target_env_msg} using the specified configuration file?"):
deploy_with_config_file(args)
46 changes: 43 additions & 3 deletions src/fabric_cli/core/fab_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,24 @@ def _is_token_defined(self, scope):
status_code=con.ERROR_AUTHENTICATION_FAILED,
)

def get_access_token(self, scope: list[str], interactive_renew=True) -> str:
def acquire_token(self, scope: list[str], interactive_renew=True) -> dict:
"""
Core MSAL token acquisition method that returns the full MSAL result.

This method contains the common authentication logic shared between
get_access_token and msal_bridge.get_token. It performs no validation
on scopes - callers are responsible for their own validation needs.

Args:
scope: The scopes for which to request the token
interactive_renew: Whether to allow interactive authentication for user flows

Returns:
Full MSAL result dictionary containing access_token, expires_on, etc.

Raises:
FabricCLIError: When token acquisition fails
"""
token = None
env_var_token = self._get_access_token_from_env_vars_if_exist(scope)

Expand Down Expand Up @@ -476,8 +493,11 @@ def get_access_token(self, scope: list[str], interactive_renew=True) -> str:
self.set_tenant(token.get("id_token_claims")["tid"])

if token and token.get("error"):
fab_logger.log_debug(
f"Error in get token: {token.get('error_description')}")
raise FabricCLIError(
ErrorMessages.Auth.access_token_error(token.get("error_description")),
ErrorMessages.Auth.access_token_error(
"Something went wrong while trying to acquire a token. Please try to run `fab auth logout` and then `fab auth login` to re-login and acquire new tokens."),
status_code=con.ERROR_AUTHENTICATION_FAILED,
)
if token is None or not token.get("access_token"):
Expand All @@ -486,7 +506,27 @@ def get_access_token(self, scope: list[str], interactive_renew=True) -> str:
status_code=con.ERROR_AUTHENTICATION_FAILED,
)

return token.get("access_token", None)
return token

def get_access_token(self, scope: list[str], interactive_renew=True) -> str | None:
"""
Get an access token string for the specified scopes.

This method maintains the existing CLI API - returns just the token string
for backward compatibility. Uses the shared acquire_token method internally.

Args:
scope: The scopes for which to request the token
interactive_renew: Whether to allow interactive authentication for user flows

Returns:
Access token string or None if acquisition fails

Raises:
FabricCLIError: When token acquisition fails
"""
token_result = self.acquire_token(scope, interactive_renew)
return token_result.get("access_token", None)

def logout(self):
self._auth_info = {}
Expand Down
4 changes: 3 additions & 1 deletion src/fabric_cli/core/fab_constant.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
FAB_VERSION = __version__

# Scopes
SCOPE_FABRIC_DEFAULT = ["https://analysis.windows.net/powerbi/api/.default"]
SCOPE_FABRIC_DEFAULT = ["https://api.fabric.microsoft.com/.default"]
SCOPE_ONELAKE_DEFAULT = ["https://storage.azure.com/.default"]
SCOPE_AZURE_DEFAULT = ["https://management.azure.com/.default"]

Expand Down Expand Up @@ -183,6 +183,7 @@
COMMAND_FS_EXPORT_DESCRIPTION = "Export an item."
COMMAND_FS_GET_DESCRIPTION = "Get workspace or item properties."
COMMAND_FS_IMPORT_DESCRIPTION = "Import an item to create or update it."
COMMAND_FS_DEPLOY_DESCRIPTION = "Deploy items using a configuration file."
COMMAND_FS_SET_DESCRIPTION = "Set workspace or item properties."
COMMAND_FS_CLEAR_DESCRIPTION = "Clear the terminal screen."
COMMAND_FS_LN_DESCRIPTION = "Create a shortcut."
Expand Down Expand Up @@ -282,6 +283,7 @@
ERROR_UNIVERSAL_SECURITY_DISABLED = "UniversalSecurityDisabled"
ERROR_SPN_AUTH_MISSING = "ServicePrincipalAuthMissing"
ERROR_JOB_FAILED = "JobFailed"
ERROR_IN_DEPLOYMENT = "DeploymentFailed"

# Exit codes
EXIT_CODE_SUCCESS = 0
Expand Down
6 changes: 1 addition & 5 deletions src/fabric_cli/core/fab_interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,13 @@
from fabric_cli.core.fab_decorators import singleton
from fabric_cli.utils import fab_commands
from fabric_cli.utils import fab_ui as utils_ui

from fabric_cli.core.fab_parser_setup import get_global_parser_and_subparsers

@singleton
class InteractiveCLI:
def __init__(self, parser=None, subparsers=None):
"""Initialize the interactive CLI."""
if parser is None or subparsers is None:
from fabric_cli.core.fab_parser_setup import (
get_global_parser_and_subparsers,
)

parser, subparsers = get_global_parser_and_subparsers()

self.parser = parser
Expand Down
130 changes: 130 additions & 0 deletions src/fabric_cli/core/fab_msal_bridge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

import os
from typing import Optional

from azure.core.credentials import AccessToken, TokenCredential
from azure.core.exceptions import ClientAuthenticationError

from fabric_cli.core import fab_constant as con
from fabric_cli.core import fab_logger
from fabric_cli.core.fab_auth import FabAuth

# Bridge-specific: strict .default scope validation
VALID_DEFATULT_SCOPES = {
con.SCOPE_FABRIC_DEFAULT[0],
}

class MsalTokenCredential(TokenCredential):
"""
A TokenCredential implementation that wraps the existing Fabric CLI MSAL authentication.

This bridge uses the CLI user's existing authentication and provides it through
the Azure Identity TokenCredential interface. It handles refresh token management
automatically via MSAL's silent acquisition flow.

The credential will use whatever authentication the CLI user has already configured:
- User authentication (from fab auth login)
- Service principal (from environment variables)
- Managed identity (when running in Azure)
- Environment tokens (pre-acquired tokens)

Args:
fab_auth: FabAuth instance containing the authentication configuration.
"""

def __init__(self, fab_auth: FabAuth):
self._fab_auth = fab_auth

def get_token(
self,
*scopes: str,
claims: Optional[str] = None,
tenant_id: Optional[str] = None,
enable_cae: bool = False,
**kwargs
) -> AccessToken:
"""
Get an access token for the specified scopes.

Args:
scopes: The scopes for which to request the token
claims: Optional claims challenge
tenant_id: Optional tenant ID (not used in this implementation)
enable_cae: Whether to enable Continuous Access Evaluation (not used)
**kwargs: Additional keyword arguments

Returns:
AccessToken object containing the token and expiration time

Raises:
ClientAuthenticationError: When authentication is not available
"""
for scope in scopes:
if scope not in VALID_DEFATULT_SCOPES:
fab_logger.log_debug(f"Invalid scope rejected: {scope}")
raise ClientAuthenticationError(
f"Security validation failed: requested scope is not supported."
f"Invalid scope: {scope}. "
f"Allowed scopes: {', '.join(VALID_DEFATULT_SCOPES)}"
)
try:
msal_result = self._fab_auth.acquire_token(
list(scopes),
interactive_renew=False # Bridge is always headless
)

return self._to_azure_access_token(msal_result)

except Exception as e:
fab_logger.log_debug(f"Token acquisition failed: {e}")
raise ClientAuthenticationError(
f"\n{str(e)}"
) from e

def _to_azure_access_token(self, msal_result: dict) -> AccessToken:
"""Convert MSAL result to AccessToken object."""
access_token = msal_result["access_token"]

# Handle expires_on - MSAL returns Unix timestamp as string or int
expires_on = msal_result.get("expires_on")
if expires_on:
if isinstance(expires_on, str):
expires_on = int(expires_on)
else:
# Fallback: calculate from expires_in if available
expires_in = msal_result.get("expires_in")
if expires_in:
import time
expires_on = int(time.time() + expires_in)
else:
raise ClientAuthenticationError(
"Token expiration time is required but not available")
return AccessToken(access_token, int(expires_on))

def close(self) -> None:
"""Close the credential (no-op for this implementation)."""
pass


def create_fabric_token_credential() -> TokenCredential:
"""
Create a TokenCredential that uses the current Fabric CLI authentication.

This function creates a TokenCredential that wraps the existing MSAL authentication
from the Fabric CLI. It will use whatever authentication the user has already
configured (user login, service principal, managed identity, or environment tokens).
Returns:
TokenCredential that can be used with Azure SDKs

Raises:
ClientAuthenticationError: When no authentication is configured
"""
fab_auth = FabAuth()

identity_type = fab_auth.get_identity_type()

fab_logger.log_debug(f"Creating TokenCredential for identity type: {identity_type}")
return MsalTokenCredential(fab_auth)

1 change: 1 addition & 0 deletions src/fabric_cli/core/fab_parser_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ def create_parser_and_subparsers():
fs_parser.register_open_parser(subparsers) # open
fs_parser.register_export_parser(subparsers) # export
fs_parser.register_import_parser(subparsers) # import
fs_parser.register_deploy_parser(subparsers) # deploy
fs_parser.register_set_parser(subparsers) # set
fs_parser.register_get_parser(subparsers) # get
fs_parser.register_clear_parser(subparsers) # clear
Expand Down
Loading