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
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
sample/workspace/
docs/how_to/parameterization.md
59 changes: 49 additions & 10 deletions docs/how_to/parameterization.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,15 @@ spark_pool:
name: "PROD-Pool-name"

semantic_model_binding:
- connection_id: "connection_id"
semantic_model_name: "semantic_model_name"
default:
connection_id:
PPE: "PPE-connection_id"
PROD: "PROD-connection_id"
models:
- semantic_model_name: "semantic_model_name"
connection_id:
PPE: "PPE-connection_id"
PROD: "PROD-connection_id"
```

Raise a [feature request](https://github.com/microsoft/fabric-cicd/issues/new?template=2-feature.yml) for additional parameterization capabilities.
Expand Down Expand Up @@ -138,19 +145,51 @@ spark_pool:

### `semantic_model_binding`

Semantic model binding is used to connect semantic models that require cloud or on-premises data sources to the appropriate connection after deployment. The `semantic_model_binding` parameter automatically configures these connections during the deployment process, ensuring your semantic models can refresh data from cloud and on-premises sources in the target environment.
Semantic model binding automatically connects semantic models to the appropriate data source connection (e.g., cloud or on-premises) after deployment, ensuring your models can refresh data in the target environment.

**Important:** The legacy format is on a deprecation path. Please migrate to the recommended format.

**Recommended format:**

```yaml
semantic_model_binding:
# Required field: value must be a string (GUID)
# Connection Ids can be found from the Fabric UI under Settings -> Manage Connections and gateways -> Settings pane of the connection
- connection_id: <connection_id>
# Required field: value must be a string or a list of strings
semantic_model_name: <semantic_model_name>
# OR
semantic_model_name: [<semantic_model_name1>,<semantic_model_name2>,...]
# Default connection for all models not explicitly listed
default:
connection_id:
PPE: <PPE-connection_guid>
PROD: <PROD-connection_guid>
# OR use _ALL_ for same connection across environments
# _ALL_: <connection_guid>

# Explicit bindings override default
models:
- semantic_model_name: "<semantic_model_name>"
connection_id:
PPE: <PPE-connection_guid>
PROD: <PROD-connection_guid>

- semantic_model_name: ["<semantic_model_name1>", "<semantic_model_name2>", ...]
connection_id:
_ALL_: <connection_guid>
```

**Legacy format:**

```yaml
# Legacy format:
semantic_model_binding:
- connection_id: <connection_guid>
# Required field: value must be a string or a list of strings
semantic_model_name: "<semantic_model_name>"
# OR
semantic_model_name: ["<semantic_model_name1>","<semantic_model_name2>", ...]
```

**Notes:**

- The `_ALL_` environment key (case-insensitive) can be used in the `connection_id` dictionary to apply the same connection to any target environment.
- Connection ID values must be valid GUIDs.

## Advanced Find and Replace

### `find_value` Regex
Expand Down
16 changes: 15 additions & 1 deletion sample/workspace/parameter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,24 @@ spark_pool:
# Optional field:
item_name:

# Deprecation notice: 'gateway_binding' will not be supported in future releases.
gateway_binding:
- gateway_id: "f96870d5-5f86-49ad-bf41-5967fd7c1c6d"
dataset_name: "gateway_sm2"


# Deprecation notice: Legacy format will not be supported in future releases.
semantic_model_binding:
- connection_id: "76e05dfe-9855-4e3d-a410-1dda048dbe99"
semantic_model_name: ["cloudconnections", "MySemanticModel_ADLS_Gen2"]

# New format (recommended): Supports default connections and per-item overrides
semantic_model_binding:
default:
connection_id:
PPE: "76e05dfe-9855-4e3d-a410-1dda048dbe99"
PROD: "c4f8e2b1-3d2a-4f5b-9c6e-7a8b9c0d1e2f"
models:
- semantic_model_name: ["cloudconnections", "MySemanticModel_ADLS_Gen2"]
connection_id:
PPE: "f96870d5-5f86-49ad-bf41-5967fd7c1c6d"
PROD: "a1b2c3d4-5678-90ab-cdef-1234567890ab"
2 changes: 1 addition & 1 deletion src/fabric_cicd/_items/_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ def _update_compute_settings(fabric_workspace_obj: FabricWorkspace, item_guid: s
parameter_dict = fabric_workspace_obj.environment_parameter["spark_pool"]
for key in parameter_dict:
instance_pool_id = key["instance_pool_id"]
replace_value = process_environment_key(fabric_workspace_obj, key["replace_value"])
replace_value = process_environment_key(fabric_workspace_obj.environment, key["replace_value"])
input_name = key.get("item_name")
if instance_pool_id == pool_id and (input_name == item_name or not input_name):
# replace any found references with specified environment value
Expand Down
174 changes: 141 additions & 33 deletions src/fabric_cicd/_items/_semanticmodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import logging

from fabric_cicd import FabricWorkspace, constants
from fabric_cicd._parameter._utils import process_environment_key

logger = logging.getLogger(__name__)

Expand All @@ -23,30 +24,140 @@ def publish_semanticmodels(fabric_workspace_obj: FabricWorkspace) -> None:
exclude_path = r".*\.pbi[/\\].*"
fabric_workspace_obj._publish_item(item_name=item_name, item_type=item_type, exclude_path=exclude_path)

model_with_binding_dict = fabric_workspace_obj.environment_parameter.get("semantic_model_binding", [])
# Bind semantic models to connections after deploying (post-deployment step)
semantic_model_binding = fabric_workspace_obj.environment_parameter.get("semantic_model_binding", {})
if semantic_model_binding:
environment = fabric_workspace_obj.environment

# Check if legacy format (list) or new format (dict)
if isinstance(semantic_model_binding, list):
binding_mapping = build_binding_mapping_legacy(fabric_workspace_obj, semantic_model_binding)
else:
binding_mapping = build_binding_mapping(fabric_workspace_obj, semantic_model_binding, environment)

if binding_mapping:
connections = get_connections(fabric_workspace_obj)
bind_semanticmodel_to_connection(
fabric_workspace_obj=fabric_workspace_obj, connections=connections, connection_details=binding_mapping
)


if not model_with_binding_dict:
return
def build_binding_mapping_legacy(fabric_workspace_obj: FabricWorkspace, semantic_model_binding: list) -> dict:
"""
Build the connection mapping from legacy list-based semantic_model_binding parameter.

# Build connection mapping from semantic_model_binding parameter
Args:
fabric_workspace_obj: The FabricWorkspace object
semantic_model_binding: The semantic_model_binding parameter as a list

Returns:
Dictionary mapping semantic model names to connection IDs
"""
logger.warning(
"The legacy 'semantic_model_binding' list format is deprecated and will be removed in a future release. "
"Please migrate to the new dictionary format with 'default' and 'models' keys. "
"See: https://microsoft.github.io/fabric-cicd/how_to/parameterization/"
)
item_type = "SemanticModel"
binding_mapping = {}
repository_models = set(fabric_workspace_obj.repository_items.get(item_type, {}).keys())

for entry in semantic_model_binding:
connection_id = entry.get("connection_id")
model_names = entry.get("semantic_model_name", [])

if not connection_id:
logger.debug("No connection_id found in semantic_model_binding entry, skipping")
continue

# Legacy format only supports string connection_id
if isinstance(connection_id, dict):
logger.warning(
"Environment-specific connection_id dictionaries are not supported in the legacy format. "
"Please migrate to the new dictionary format to use environment-specific values."
)
continue

if isinstance(model_names, str):
model_names = [model_names]

for name in model_names:
if name not in repository_models:
logger.warning(f"Semantic model '{name}' specified in parameter.yml not found in repository")
continue
binding_mapping[name] = connection_id

return binding_mapping


def build_binding_mapping(
fabric_workspace_obj: FabricWorkspace, semantic_model_binding: dict, environment: str
) -> dict:
"""
Build the connection mapping from semantic_model_binding parameter.

The new format requires environment-specific connection_id values (use '_ALL_' for all environments).

Supports:
- default.connection_id: Applied to all models in the repository that are not explicitly listed
- models: List of explicit model-to-connection mappings

Args:
fabric_workspace_obj: The FabricWorkspace object
semantic_model_binding: The semantic_model_binding parameter dictionary
environment: The target environment name (_ALL_ key can be used)

for model in model_with_binding_dict:
model_name = model.get("semantic_model_name", [])
connection_id = model.get("connection_id")
Returns:
Dictionary mapping semantic model names to connection IDs
"""
item_type = "SemanticModel"
binding_mapping = {}
repository_models = set(fabric_workspace_obj.repository_items.get(item_type, {}).keys())

# Get default connection_id for this environment
default_connection_id = None
default_config = semantic_model_binding.get("default", {})
if default_config:
connection_id_config = default_config.get("connection_id", {})
connection_id_config = process_environment_key(fabric_workspace_obj, connection_id_config)
default_connection_id = connection_id_config.get(environment)
if not default_connection_id:
logger.debug(f"Environment '{environment}' not found in default.connection_id")

# Process explicit model bindings
explicit_models = set()
models_config = semantic_model_binding.get("models", [])

for model in models_config:
model_names = model.get("semantic_model_name", [])
connection_id_config = model.get("connection_id", {})

if isinstance(model_names, str):
model_names = [model_names]

connection_id_config = process_environment_key(fabric_workspace_obj, connection_id_config)
connection_id = connection_id_config.get(environment)
if not connection_id:
logger.debug(f"Environment '{environment}' not found in connection_id for semantic model(s): {model_names}")
continue

if isinstance(model_name, str):
model_name = [model_name]
# Only track as explicit if environment connection is defined
explicit_models.update(model_names)

for name in model_name:
for name in model_names:
if name not in repository_models:
logger.warning(f"Semantic model '{name}' specified in parameter.yml not found in repository")
continue
binding_mapping[name] = connection_id

connections = get_connections(fabric_workspace_obj)
# Apply default connection to non-explicit models
if default_connection_id:
default_models = repository_models - explicit_models
for model_name in default_models:
binding_mapping[model_name] = default_connection_id
logger.debug(f"Applying default connection to semantic model '{model_name}'")

if binding_mapping:
bind_semanticmodel_to_connection(
fabric_workspace_obj=fabric_workspace_obj, connections=connections, connection_details=binding_mapping
)
return binding_mapping


def get_connections(fabric_workspace_obj: FabricWorkspace) -> dict:
Expand All @@ -59,6 +170,7 @@ def get_connections(fabric_workspace_obj: FabricWorkspace) -> dict:
Returns:
Dictionary with connection ID as key and connection details as value
"""
# https://learn.microsoft.com/en-us/rest/api/fabric/core/connections/list-connections
connections_url = f"{constants.FABRIC_API_ROOT_URL}/v1/connections"

try:
Expand Down Expand Up @@ -90,36 +202,31 @@ def bind_semanticmodel_to_connection(
Args:
fabric_workspace_obj: The FabricWorkspace object containing the items to be published.
connections: Dictionary of connection objects with connection ID as key.
connection_details: Dictionary mapping dataset names to connection IDs from parameter.yml.
connection_details: Dictionary mapping semantic model names to connection IDs from parameter.yml.
"""
item_type = "SemanticModel"

# Loop through each semantic model in the semantic_model_binding section
for dataset_name, connection_id in connection_details.items():
for model_name, connection_id in connection_details.items():
# Check if the connection ID exists in the connections dict
if connection_id not in connections:
logger.warning(f"Connection ID '{connection_id}' not found for semantic model '{dataset_name}'")
continue

# Check if this semantic model exists in the repository
if dataset_name not in fabric_workspace_obj.repository_items.get(item_type, {}):
logger.warning(f"Semantic model '{dataset_name}' not found in repository")
logger.warning(f"Connection ID '{connection_id}' not found for semantic model '{model_name}'")
continue

# Get the semantic model object
item_obj = fabric_workspace_obj.repository_items[item_type][dataset_name]
# Get the semantic model object (validated during binding mapping creation)
item_obj = fabric_workspace_obj.repository_items[item_type][model_name]
model_id = item_obj.guid

logger.info(f"Binding semantic model '{dataset_name}' (ID: {model_id}) to connection '{connection_id}'")
logger.info(f"Binding semantic model '{model_name}' (ID: {model_id}) to connection '{connection_id}'")

try:
# Get the connection details for this semantic model from Fabric API
# https://learn.microsoft.com/en-us/rest/api/fabric/core/items/list-item-connections
item_connections_url = f"{constants.FABRIC_API_ROOT_URL}/v1/workspaces/{fabric_workspace_obj.workspace_id}/items/{model_id}/connections"
connections_response = fabric_workspace_obj.endpoint.invoke(method="GET", url=item_connections_url)
connections_data = connections_response.get("body", {}).get("value", [])

if not connections_data:
logger.warning(f"No connections found for semantic model '{dataset_name}'")
logger.debug(f"No existing connections found for semantic model '{model_name}', skipping binding")
continue

# Use the first connection as the template
Expand All @@ -134,22 +241,23 @@ def bind_semanticmodel_to_connection(
request_body = build_request_body({"connectionBinding": connection_binding})

# Make the bind connection API call
powerbi_url = f"{constants.FABRIC_API_ROOT_URL}/v1/workspaces/{fabric_workspace_obj.workspace_id}/semanticModels/{model_id}/bindConnection"
# https://learn.microsoft.com/en-us/rest/api/fabric/semanticmodel/items/bind-semantic-model-connection
binding_url = f"{constants.FABRIC_API_ROOT_URL}/v1/workspaces/{fabric_workspace_obj.workspace_id}/semanticModels/{model_id}/bindConnection"
bind_response = fabric_workspace_obj.endpoint.invoke(
method="POST",
url=powerbi_url,
url=binding_url,
body=request_body,
)

status_code = bind_response.get("status_code")

if status_code == 200:
logger.info(f"Successfully bound semantic model '{dataset_name}' to connection '{connection_id}'")
logger.info(f"Successfully bound semantic model '{model_name}' to connection '{connection_id}'")
else:
logger.warning(f"Failed to bind semantic model '{dataset_name}'. Status code: {status_code}")
logger.warning(f"Failed to bind semantic model '{model_name}'. Status code: {status_code}")

except Exception as e:
logger.error(f"Failed to bind semantic model '{dataset_name}' to connection: {e!s}")
logger.error(f"Failed to bind semantic model '{model_name}' to connection: {e!s}")
continue


Expand Down
Loading