Skip to content

Commit 04d7d4d

Browse files
author
aviat cohen
committed
Merge branch 'dev/aviatcohen/supportPrintKeyValue' of https://github.com/aviatco/fabric-cli into dev/aviatcohen/supportPrintKeyValue
2 parents e0e3e24 + a9c1430 commit 04d7d4d

16 files changed

+11139
-19
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
kind: added
2+
body: support mv/cp/export/import for SqlDatabase
3+
time: 2025-11-11T14:46:02.635689113Z
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
kind: fixed
2+
body: Fix create connection with onPremGateway and encryptedCredentials
3+
time: 2025-11-11T13:42:54.695045434Z

docs/examples/connection_examples.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ Create a connection that uses a specific gateway for secure access.
4646
```
4747
fab create .connections/conn.Connection -P gateway=MyVnetGateway.Gateway,connectionDetails.type=SQL,connectionDetails.parameters.server=<server>,connectionDetails.parameters.database=sales,credentialDetails.type=Basic,credentialDetails.username=<username>,credentialDetails.password=<password>
4848
```
49+
#### Create Connection with On-premises Gateway
50+
51+
Create a connection that uses a specific on-premises gateway with encrypted credentials for secure access
52+
53+
```
54+
fab create .connections/conn.Connection -P gateway=MyVnetGateway.Gateway,connectionDetails.type=SQL,connectionDetails.parameters.server=<server>,connectionDetails.parameters.database=sales,credentialDetails.type=Basic,credentialDetails.values=[{"gatewayId":"<gatewayId>", "encryptedCredentials": "<encryptedCredentials>"}]
55+
```
4956

5057
#### Create Connection with All Parameters
5158

src/fabric_cli/core/fab_config/command_support.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ commands:
167167
- graph_ql_api
168168
#- lakehouse # this is included just to support underlying /Files
169169
- dataflow
170+
- sql_database
170171
cp:
171172
supported_elements:
172173
- workspace
@@ -192,6 +193,7 @@ commands:
192193
- graph_ql_api
193194
#- lakehouse # this is included just to support underlying /Files
194195
- dataflow
196+
- sql_database
195197
ln:
196198
supported_elements:
197199
- onelake
@@ -242,6 +244,7 @@ commands:
242244
- variable_library
243245
- graph_ql_api
244246
- dataflow
247+
- sql_database
245248
import:
246249
supported_items:
247250
- report
@@ -262,6 +265,7 @@ commands:
262265
- variable_library
263266
- graph_ql_api
264267
- dataflow
268+
- sql_database
265269
get:
266270
supported_elements:
267271
- workspace

src/fabric_cli/errors/common.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,13 @@ def missing_connection_creation_method_or_parameters(
195195
supported_creation_methods: list,
196196
) -> str:
197197
return f"Missing connection creation method and parameters. Please indicate either one of the following creation methods: {supported_creation_methods}, or provide parameters for automatic selection"
198+
199+
@staticmethod
200+
def missing_onpremises_gateway_parameters(
201+
missing_params: list,
202+
) -> str:
203+
return f"Missing parameters for credential values in OnPremisesGateway connectivity type: {missing_params}"
204+
205+
@staticmethod
206+
def invalid_onpremises_gateway_values() -> str:
207+
return "Values must be a list of JSON objects, each containing 'gatewayId' and 'encryptedCredentials' keys"

src/fabric_cli/utils/fab_cmd_mkdir_utils.py

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -359,33 +359,41 @@ def check_required_params(params, required_params):
359359
)
360360

361361

362-
def _validate_credential_params(cred_type, provided_cred_params):
363-
ignored_params = []
364-
params = {}
362+
def _get_params_per_cred_type(cred_type, is_on_premises_gateway):
365363
match cred_type:
366364
case "Anonymous" | "WindowsWithoutImpersonation" | "WorkspaceIdentity":
367-
param_keys = []
365+
return []
368366
case "Basic" | "Windows":
369-
param_keys = ["username", "password"]
367+
if is_on_premises_gateway:
368+
return ["values"]
369+
else:
370+
return ["username", "password"]
370371
case "Key":
371-
param_keys = ["key"]
372+
return ["key"]
372373
case "OAuth2":
373374
raise FabricCLIError(
374375
"OAuth2 credential type is not supported",
375376
fab_constant.ERROR_NOT_SUPPORTED,
376377
)
377378
case "ServicePrincipal":
378-
param_keys = [
379+
return [
379380
"servicePrincipalClientId",
380381
"servicePrincipalSecret",
381382
"tenantId",
382383
]
383384
case "SharedAccessSignature":
384-
param_keys = ["token"]
385+
return ["token"]
385386
case _:
386387
utils_ui.print_warning(
387388
f"Unsupported credential type {cred_type}. Skipping validation"
388389
)
390+
return []
391+
392+
393+
def _validate_credential_params(cred_type, provided_cred_params, is_on_premises_gateway):
394+
ignored_params = []
395+
params = {}
396+
param_keys = _get_params_per_cred_type(cred_type, is_on_premises_gateway)
389397

390398
missing_params = [
391399
key for key in param_keys if key.lower() not in provided_cred_params
@@ -405,12 +413,46 @@ def _validate_credential_params(cred_type, provided_cred_params):
405413
utils_ui.print_warning(
406414
f"Ignoring unsupported parameters for credential type {cred_type}: {ignored_params}"
407415
)
416+
if is_on_premises_gateway:
417+
provided_cred_params["values"] = _validate_and_get_on_premises_gateway_credential_values(provided_cred_params.get("values"))
408418

409419
for key in param_keys:
410420
params[key] = provided_cred_params[key.lower()]
411421

412422
return params
413423

424+
def _validate_and_get_on_premises_gateway_credential_values(cred_values):
425+
for item in cred_values:
426+
if not isinstance(item, dict):
427+
raise FabricCLIError(
428+
ErrorMessages.Common.invalid_onpremises_gateway_values(),
429+
fab_constant.ERROR_INVALID_INPUT,
430+
)
431+
432+
param_values_keys = ["gatewayId", "encryptedCredentials"]
433+
missing_params = [
434+
key for key in param_values_keys
435+
if not all(key.lower() in {k.lower() for k in item.keys()} for item in cred_values)
436+
]
437+
if len(missing_params) > 0:
438+
raise FabricCLIError(
439+
ErrorMessages.Common.missing_onpremises_gateway_parameters(missing_params),
440+
fab_constant.ERROR_INVALID_INPUT,
441+
)
442+
443+
ignored_params = [
444+
key
445+
for item in cred_values
446+
for key in item.keys()
447+
if key not in [k.lower() for k in param_values_keys]
448+
]
449+
if len(ignored_params) > 0:
450+
utils_ui.print_warning(
451+
f"Ignoring unsupported parameters for on-premises gateway: {ignored_params}"
452+
)
453+
454+
return [{key: item[key.lower()] for key in param_values_keys if key.lower() in item} for item in cred_values]
455+
414456

415457
def get_connection_config_from_params(payload, con_type, con_type_def, params):
416458
connection_request = payload
@@ -537,13 +579,6 @@ def get_connection_config_from_params(payload, con_type, con_type_def, params):
537579
fab_constant.ERROR_INVALID_INPUT,
538580
)
539581

540-
if missing_params:
541-
missing_params_str = ", ".join(missing_params)
542-
raise FabricCLIError(
543-
f"Missing parameter(s) {missing_params_str} for creation method {c_method}",
544-
fab_constant.ERROR_INVALID_INPUT,
545-
)
546-
547582
connection_request["connectionDetails"] = {
548583
"type": con_type,
549584
"creationMethod": creation_method["name"],
@@ -563,6 +598,14 @@ def get_connection_config_from_params(payload, con_type, con_type_def, params):
563598
"password": "********"
564599
}
565600
}
601+
or in case of OnPremisesGateway:
602+
"credentialDetails": {
603+
.....,
604+
"credentials": {
605+
"credentialType": "Basic",
606+
"values": [{gatewayId: "gatewayId", encryptedCredentials: "**********"}]
607+
}
608+
}
566609
"""
567610
sup_cred_types = ", ".join(con_type_def["supportedCredentialTypes"])
568611
if not params.get("credentialdetails"):
@@ -603,7 +646,8 @@ def get_connection_config_from_params(payload, con_type, con_type_def, params):
603646
if "skiptestconnection" in provided_cred_params:
604647
provided_cred_params.pop("skiptestconnection")
605648

606-
connection_params = _validate_credential_params(cred_type, provided_cred_params)
649+
is_on_premises_gateway = connection_request.get("connectivityType").lower() == "onpremisesgateway"
650+
connection_params = _validate_credential_params(cred_type, provided_cred_params, is_on_premises_gateway)
607651

608652
connection_request["credentialDetails"] = {
609653
"singleSignOnType": singleSignOnType,
@@ -613,6 +657,9 @@ def get_connection_config_from_params(payload, con_type, con_type_def, params):
613657
}
614658
connection_request["credentialDetails"]["credentials"]["credentialType"] = cred_type
615659

660+
if is_on_premises_gateway:
661+
connection_request["credentialDetails"]["credentials"]["values"] = connection_params.get("values")
662+
616663
return connection_request
617664

618665

src/fabric_cli/utils/fab_util.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,25 @@ def get_dict_from_parameter(
121121
key, rest = param.split(".", 1)
122122
return {key: get_dict_from_parameter(rest, value, max_depth, current_depth + 1)}
123123
else:
124-
clean_value = value.replace("'", "").replace('"', "")
124+
clean_value = try_get_json_value_from_string(value)
125125
return {param: clean_value}
126126

127+
def try_get_json_value_from_string(value: str) -> Any:
128+
"""
129+
Try to parse a string as JSON, with special handling for array parameters.
130+
131+
Args:
132+
value: String that may contain JSON data
133+
134+
Returns:
135+
Parsed JSON if valid, otherwise original string
136+
"""
137+
if value.strip().startswith('[{') and value.strip().endswith('}]'):
138+
try:
139+
return json.loads(value)
140+
except json.JSONDecodeError:
141+
pass
142+
return value.replace("'", "").replace('"', "")
127143

128144
def merge_dicts(dict1: dict, dict2: dict) -> dict:
129145
"""

tests/authoring_tests.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,14 @@ For most testing scenarios, the mock values provided in [`static_test_data.py`](
9393
| `FABRIC_CLI_TEST_CREDENTIAL_DETAILS_USERNAME` | Username for Connection credential details | Username for Connection creation tests |
9494
| `FABRIC_CLI_TEST_CREDENTIAL_DETAILS_PASSWORD` | Password for Connection credentialdetails | Password for Connection creation tests|
9595

96+
### On-premesis gateway Details
97+
98+
| Variable | Description | Purpose |
99+
|----------|-------------|---------|
100+
| `FABRIC_CLI_TEST_ONPREMISES_GATEWAY_ID` | On-premesis gateway Id for Connection credential details | Gateway Id for Connection creation tests |
101+
| `FABRIC_CLI_TEST_ONPREMISES_GATEWAY_ENCRYPTED_CREDENTIALS` | On-premesis gateway encrypted credential for Connection encrypted credential details | Encrypted credential for Connection creation tests|
102+
103+
96104
### Mock vs. Actual Values - Best Practices
97105

98106
| Resource Type | Recommended Approach | Example Mock Value | When to Use Actual |

tests/test_commands/api_processors/connection_api_processor.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33

44
import json
55
from tests.test_commands.api_processors.base_api_processor import BaseAPIProcessor
6-
from tests.test_commands.api_processors.utils import load_response_json_body
6+
from tests.test_commands.api_processors.utils import (
7+
load_request_json_body,
8+
load_response_json_body,
9+
)
10+
from tests.test_commands.data.static_test_data import get_mock_data, get_static_data
711

812

913
class ConnectionAPIProcessor(BaseAPIProcessor):
@@ -13,6 +17,21 @@ def __init__(self, generated_name_mapping):
1317
self.generated_name_mapping = generated_name_mapping
1418

1519
def try_process_request(self, request) -> bool:
20+
uri = request.uri
21+
self._mock_gateway_id_in_uri(request)
22+
23+
# Handle connection creation and listing
24+
if uri.lower() == self.CONNECTIONS_URI.lower():
25+
method = request.method
26+
if method == "POST":
27+
"""https://learn.microsoft.com/en-us/rest/api/fabric/core/connections/create-connection?tabs=HTTP"""
28+
self._handle_post_request(request)
29+
return True
30+
31+
# Handle supported connection types with gateway ID query parameter
32+
if uri.lower().startswith(f"{self.CONNECTIONS_URI.lower()}/supportedconnectiontypes"):
33+
return True
34+
1635
return False
1736

1837
def try_process_response(self, request, response) -> bool:
@@ -23,6 +42,9 @@ def try_process_response(self, request, response) -> bool:
2342
if method == "GET":
2443
"""https://learn.microsoft.com/en-us/rest/api/fabric/core/connections/list-connections?tabs=HTTP"""
2544
self._handle_get_response(response)
45+
if method == "POST":
46+
"""https://learn.microsoft.com/en-us/rest/api/fabric/core/connections/create-connection?tabs=HTTP"""
47+
self._handle_post_response(response)
2648
return True
2749
return False
2850

@@ -34,9 +56,56 @@ def _handle_get_response(self, response):
3456
new_value = []
3557
for item in data["value"]:
3658
if item.get("displayName") in self.generated_name_mapping:
59+
self._mock_gateway_references(item)
3760
new_value.append(item)
3861

3962
data["value"] = new_value
4063

4164
new_body_str = json.dumps(data)
4265
response["body"]["string"] = new_body_str.encode("utf-8")
66+
67+
def _handle_post_request(self, request):
68+
"""Handle POST request for connection creation"""
69+
data = load_request_json_body(request)
70+
if not data:
71+
return
72+
73+
self._mock_gateway_references(data)
74+
75+
new_body_str = json.dumps(data)
76+
request.body = new_body_str
77+
78+
def _handle_post_response(self, response):
79+
"""Handle POST response for connection creation"""
80+
data = load_response_json_body(response)
81+
if not data:
82+
return
83+
84+
self._mock_gateway_references(data)
85+
86+
new_body_str = json.dumps(data)
87+
response["body"]["string"] = new_body_str.encode("utf-8")
88+
89+
def _mock_gateway_references(self, obj):
90+
"""Mock gateway ID references in connection objects"""
91+
static_gateway_id = get_static_data().onpremises_gateway_details.id
92+
mock_gateway_id = get_mock_data().onpremises_gateway_details.id
93+
94+
# Mock direct gatewayId field
95+
if "gatewayId" in obj and obj["gatewayId"] == static_gateway_id:
96+
obj["gatewayId"] = f"{mock_gateway_id}"
97+
98+
# Mock gatewayId in credentialDetails.values arrays
99+
if "credentialDetails" in obj and "values" in obj["credentialDetails"]:
100+
for cred_value in obj["credentialDetails"]["values"]:
101+
if isinstance(cred_value, dict) and "gatewayId" in cred_value:
102+
if cred_value["gatewayId"] == static_gateway_id:
103+
cred_value["gatewayId"] = mock_gateway_id
104+
105+
def _mock_gateway_id_in_uri(self, request):
106+
"""Mock gateway IDs in request URIs and query parameters"""
107+
static_gateway_id = get_static_data().onpremises_gateway_details.id
108+
mock_gateway_id = get_mock_data().onpremises_gateway_details.id
109+
110+
# Replace gateway ID in URI path and query parameters
111+
request.uri = request.uri.replace(static_gateway_id, mock_gateway_id)

0 commit comments

Comments
 (0)