From 48394f134c3aba936721fc314b7462a97034db0d Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Mon, 5 Dec 2022 12:46:12 -0600 Subject: [PATCH 01/59] Remove usePurviewType experimental flag --- docs/configuration.md | 1 - .../Helpers/PurviewCustomType.cs | 2 -- .../Parser/Settings/AppConfigurationSettings.cs | 1 - .../Services/PurviewIngestion.cs | 17 ++++------------- 4 files changed, 4 insertions(+), 17 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index b045b84..25ce4e6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -30,7 +30,6 @@ The following app settings are experimental and may be removed in future release | App Setting| Default Value in Code| Note| |----|----|----| |useResourceSet|true|Experimental feature| -|usePurviewTypes|false| Experimental feature| |maxQueryPlanSize|null|If the query plan bytes is greater than this value it will be removed from the databricks_process| |prioritizeFirstResourceSet|true|When matching against existing assets, the first resource set found will be prioritized over other assets like folders or purview custom connector entities.| |Spark_Entities|databricks_workspace;databricks_job;databricks_notebook;databricks_notebook_task|| diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs index 1385397..c22f19a 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs @@ -30,7 +30,6 @@ public class PurviewCustomType private AppConfigurationSettings? config = new AppConfigurationSettings(); public JObject? Fullentity = new JObject(); bool useResourceSet = true; - bool usePurviewTypes = false; /// /// Property that contains all Json attributes for the Custom data Entity in Microsoft Purview /// @@ -54,7 +53,6 @@ public PurviewCustomType(string name, string typeName, string qualified_name, st _logger = logger; _client = client; useResourceSet = config!.useResourceSet; - usePurviewTypes = config!.useResourceSet; Init(name , typeName diff --git a/function-app/adb-to-purview/src/Function.Domain/Models/Parser/Settings/AppConfigurationSettings.cs b/function-app/adb-to-purview/src/Function.Domain/Models/Parser/Settings/AppConfigurationSettings.cs index 4200a32..26f6b7a 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Models/Parser/Settings/AppConfigurationSettings.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Models/Parser/Settings/AppConfigurationSettings.cs @@ -26,7 +26,6 @@ public class AppConfigurationSettings public string? ClientSecret { get; set; } public string? TenantId { get; set; } public string EventHubConsumerGroup { get; set; } = "read"; - public bool usePurviewTypes { get; set; } = false; public bool useResourceSet { get; set; } = true; public string AuthEndPoint { get; set; } = "https://login.microsoftonline.com/"; public string Authority diff --git a/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs b/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs index a7a2c02..ba40263 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs @@ -24,7 +24,6 @@ namespace Function.Domain.Services public class PurviewIngestion : IPurviewIngestion { private bool useResourceSet = bool.Parse(Environment.GetEnvironmentVariable("useResourceSet") ?? "true"); - private bool usePurviewTypes = bool.Parse(Environment.GetEnvironmentVariable("usePurviewTypes") ?? "false"); private PurviewClient _purviewClient; private Int64 initGuid = -1000; //stores all mappings of columns for Origin and destination assets @@ -159,8 +158,7 @@ public async Task SendToPurview(JObject json) { if (newEntity.is_dummy_asset) { - if (!usePurviewTypes) - newEntity.Properties["attributes"]!["qualifiedName"] = newEntity.Properties["attributes"]!["qualifiedName"]!.ToString().ToLower(); + newEntity.Properties["attributes"]!["qualifiedName"] = newEntity.Properties["attributes"]!["qualifiedName"]!.ToString().ToLower(); tempEntities.Add(newEntity.Properties); } } @@ -308,16 +306,9 @@ private async Task SetOutputInput(JObject outPutInput, string QueryValeuModel sourceJson = await sourceEntity.QueryInPurview(); if (sourceEntity.is_dummy_asset) { - if (usePurviewTypes) - { - outPutInput["typeName"] = originalTypeName; - sourceEntity.Properties["typeName"] = originalTypeName; - } - else - { - outPutInput["typeName"] = sourceEntity.Properties["typeName"]; - outPutInput["uniqueAttributes"]!["qualifiedName"] = sourceEntity.Properties!["attributes"]!["qualifiedName"]!.ToString().ToLower(); - } + outPutInput["typeName"] = sourceEntity.Properties["typeName"]; + outPutInput["uniqueAttributes"]!["qualifiedName"] = sourceEntity.Properties!["attributes"]!["qualifiedName"]!.ToString().ToLower(); + inputs_outputs.Add(sourceEntity); Log("Info", $"{inorout} Entity: {qualifiedName} Type: {typename}, Not found, Creating Dummy Entity"); } From b6b48a3f1ca3bffaef2618d83e54120eeee74e88 Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Tue, 6 Dec 2022 08:50:09 -0600 Subject: [PATCH 02/59] Prioritize resource sets and allow for matching against dfs and blob resource sets --- .../Helpers/PurviewCustomType.cs | 50 ++++++++++++++----- .../Services/PurviewIngestion.cs | 10 ++++ 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs index c22f19a..2a3fbca 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs @@ -61,7 +61,7 @@ public PurviewCustomType(string name, string typeName, string qualified_name, st , description , guid ); - _logger.LogInformation($"New Entity Initialized in the process with a passed Purview Client: Nome:{name} - qualified_name:{qualified_name} - Guid:{guid}"); + _logger.LogInformation($"New Entity Initialized in the process with a passed Purview Client: Nome:{name} - qualified_name:{qualified_name} - Type: {typeName} - Guid:{guid}"); } /// /// Creation of a Microsoft Purview Custom Type entity that initialize all attributes needed @@ -339,12 +339,17 @@ public async Task QueryInPurview(string TypeName) } } + String _fqn = properties!["attributes"]!["qualifiedName"]!.ToString(); List results = await this._client.Query_entities(filter["filter"]!); + _logger.LogDebug($"Existing Asset Match Search for {_fqn}: Found {results.Count} candidate matches"); if (results.Count > 0) { + _logger.LogDebug($"Existing Asset Match Search for {_fqn}: The first match has a fqn of {results[0].qualifiedName} and type of {results[0].entityType}"); List validentity = await SelectReturnEntity(results); + _logger.LogDebug($"Existing Asset Match Search for {_fqn}: Found {validentity.Count} valid entity matches"); if (validentity.Count > 0) { + _logger.LogDebug($"Existing Asset Match Search for {_fqn}: The first valid match has a fqn of {validentity[0].qualifiedName} and type of {validentity[0].entityType}"); obj = validentity[0]; properties["guid"] = validentity[0].id; properties["typeName"] = validentity[0].entityType; @@ -352,9 +357,16 @@ public async Task QueryInPurview(string TypeName) this.Fullentity = await this._client.GetByGuid(validentity[0].id); this.is_dummy_asset = false; } + // If there are matches but there are none that are valid, it should still be a dummy asset + else + { + _logger.LogDebug($"Existing Asset Match Search for {_fqn}: Changing type to dummy type because zero valid entities"); + properties["typeName"] = EntityType; + } } else { + _logger.LogDebug($"Existing Asset Match Search for {_fqn}: Changing type to dummy type because zero search results in general"); properties["typeName"] = EntityType; } return obj; @@ -362,10 +374,10 @@ public async Task QueryInPurview(string TypeName) private async Task> SelectReturnEntity(List results) { List validEntities = new List(); - bool resourceSetHasBeenSeen = false; + bool matchingResourceSetHasBeenSeen = false; foreach (QueryValeuModel entity in results) { - _logger.LogDebug($"Working on {entity.entityType} with score {entity.SearchScore}"); + _logger.LogDebug($"Validating {this.to_compare_QualifiedName} vs {entity.qualifiedName} - Type: {entity.entityType} search score: {entity.SearchScore}"); if (IsSpark_Entity(entity.entityType)) if (results[0].qualifiedName.ToLower().Trim('/') != this.properties!["attributes"]!["qualifiedName"]!.ToString().ToLower().Trim('/')) { @@ -376,21 +388,26 @@ private async Task> SelectReturnEntity(List diff --git a/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs b/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs index ba40263..0fe5cad 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs @@ -271,8 +271,18 @@ private async Task Validate_Entities(JObject Process) QueryValeuModel sourceJson = await sourceEntity.QueryInPurview(); Process["guid"] = sourceEntity.Properties["guid"]; + + String proctype = Process["typeName"]!.ToString(); + if (sourceEntity.Properties.ContainsKey("typeName")){ + String sourcetype = sourceEntity.Properties["typeName"]!.ToString(); + Log("Info", $"PQN:{qualifiedName} Process Type name is {proctype} and sourceEntity original TypeName was {sourcetype}"); + }else{ + Log("Info", $"PQN:{qualifiedName} Process Type name is {proctype} and sourceEntity original TypeName was not set"); + } + if (sourceEntity.is_dummy_asset) { + Log("Info", "IN DUMMY ASSET AND ABOUT TO OVERWRITE"); sourceEntity.Properties["typeName"] = Process["typeName"]!.ToString(); if (!entities.ContainsKey(qualifiedName)) entities.Add(qualifiedName, sourceEntity); From fa5c9c490e4ef796e26bd11a4500751438c3666e Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Mon, 12 Dec 2022 11:21:23 -0600 Subject: [PATCH 03/59] Adding recent troubleshooting guidance --- TROUBLESHOOTING.md | 51 +++++++++++++++++++++++++++++++++ deploy-base.md | 2 ++ deploy-demo.md | 4 +++ docs/powershell-alternatives.md | 25 ++++++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 docs/powershell-alternatives.md diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 1a9fd31..a71cc82 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -48,6 +48,57 @@ In this case, use the databricks CLI to upload the jar to the expected location to avoid changes in the file name. +* ### Internal Error Resolving Secrets + + For the demo deployment, if your cluster fails and returns the error "Internal Error resolving secrets" and "Failed to fetch secrets referred to in Spark Conf", the deployment script may have failed to add an Access Policy to the Azure Key Vault or the secret scope was not created. + + **Solution**: Update the values in the below script and execute it in the cloud shell. This script deletes the demo deployment's secret scope and then recreates it. After executing the script, you should see an access policy for "AzureDatabricks" in your Azure Key Vault. + + ```bash + adb_ws_url=adb-DATABRICKS_WORKSPACE.ID.azuredatabricks.net + global_adb_token=$(az account get-access-token --resource 2ff814a6-3304-4ab8-85cb-cd0e6f879c1d -o tsv --query '[accessToken]') + adb_ws_id=/subscriptions/SUBSCRIPTION_ID/resourceGroups/RESOURCE_GROUP_NAME/providers/Microsoft.Databricks/workspaces/DATABRICKS_WORKSPACE_NAME + subscription_id=123acb-456-def + akv_name=AKV_NAME + akv_resource_id=/subscriptions/SUBSCRIPTION_ID/resourceGroups/RESOURCE_GROUP_NAME/providers/Microsoft.KeyVault/vaults/AKV_NAME + + # Remove the Secret Scope if it exists + cat << EOF > delete-scope.json + { + "scope": "purview-to-adb-kv" + } + EOF + + curl \ + -X POST https://$adb_ws_url/api/2.0/secrets/scopes/delete \ + -H "Authorization: Bearer $global_adb_token" \ + -H "X-Databricks-Azure-Workspace-Resource-Id: $adb_ws_id" \ + --data @delete-scope.json + + # If the above fails, that's okay + # Ultimately, we just need a clean slate + + cat << EOF > create-scope.json + { + "scope": "purview-to-adb-kv", + "scope_backend_type": "AZURE_KEYVAULT", + "backend_azure_keyvault": + { + "resource_id": "$akv_resource_id", + "dns_name": "https://$akv_name.vault.azure.net/" + }, + "initial_manage_principal": "users" + } + EOF + + + curl \ + -X POST https://$adb_ws_url/api/2.0/secrets/scopes/create \ + -H "Authorization: Bearer $global_adb_token" \ + -H "X-Databricks-Azure-Workspace-Resource-Id: $adb_ws_id" \ + --data @create-scope.json + ``` + ## I don't see lineage in Microsoft Purview * ### Try Refreshing the Page diff --git a/deploy-base.md b/deploy-base.md index 6cde828..b1456ec 100644 --- a/deploy-base.md +++ b/deploy-base.md @@ -117,6 +117,8 @@ From the [Azure Portal](https://portal.azure.com) echo $purview_type_resp_custom_type ``` + + If you need a Powershell alternative, see the [docs](./docs/powershell-alternatives.md#upload-custom-types). ## Download the OpenLineage Spark agent and configure with your Azure Databricks clusters diff --git a/deploy-demo.md b/deploy-demo.md index a0c69aa..ef0435f 100644 --- a/deploy-demo.md +++ b/deploy-demo.md @@ -120,3 +120,7 @@ purview_type_resp_custom_type=$(curl -s -X POST $purview_endpoint/catalog/api/at echo $purview_type_resp_custom_type ``` + +If you need a Powershell alternative, see the [docs](./docs/powershell-alternatives.md#upload-custom-types). + +You should now be able to run your demo notebook and receive lineage. diff --git a/docs/powershell-alternatives.md b/docs/powershell-alternatives.md new file mode 100644 index 0000000..258100c --- /dev/null +++ b/docs/powershell-alternatives.md @@ -0,0 +1,25 @@ +# Powershell Alternative Scripts + +In some cases, you're not able to use the cloud shell or you don't have access to a machine that can run wsl / curl. This doc provides alternatives to select + +## Upload Custom Types + +Assumes you are in the `deployment/infra` folder of the repo. + +```powershell +$purview_endpoint="https://PURVIEW_ACCOUNT_NAME.purview.azure.com" +$TENANT_ID="TENANT_ID" +$CLIENT_ID="CLIENT_ID" +$CLIENT_SECRET="CLIENT_SECRET" + +$get_token=(Invoke-RestMethod -Method 'Post' -Uri "https://login.microsoftonline.com/$TENANT_ID/oauth2/token" -Body "resource=https://purview.azure.net&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&grant_type=client_credentials") +$token=$get_token.access_token +$body=(Get-Content -Path .\Custom_Types.json) +$headers = @{ +'Content-Type'='application/json' +'Authorization'= "Bearer $token" +} + +Invoke-RestMethod -Method 'Post' -Uri "$purview_endpoint/catalog/api/atlas/v2/types/typedefs" -Body $body -Headers $headers + +``` From 9c71e45e87f81a577ae413efef06d489a10556b4 Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Mon, 12 Dec 2022 00:25:53 -0600 Subject: [PATCH 04/59] Testing Overhaul * Adding tests/environment folder to store datasets and bicep templates for test sources * Added scripts to create databricks jobs and a notebook to mount storage on Databricks * Making test environments more consistent across notebooks (secret scope, environment variables) * Handle of tests were modified to correct mistakes not caught in source controlled versions * Added documentation for testing environment including what secrets are used and what they look like * Adding requirements.txt file for environment deployment * Hive tests should run without additional intervention (i.e. use CREATE IF NOT EXISTS) * Removing production env deployment * Remove the wasbs with parameters test * After updating all jobdefs to be ready for upload, the run-tests script needed to look at .name instead of .settings.name * Unfortunately, when calling the jobs API, it returns a .settings.name which must be used --- .github/workflows/build-release.yml | 37 +- .gitignore | 1 + tests/environment/README.md | 122 ++++++ tests/environment/config.json | 366 +++++++++++++++++ tests/environment/datasets/azsql.sql | 27 ++ tests/environment/datasets/make-data.py | 112 ++++++ tests/environment/datasets/sqlpool.sql | 30 ++ tests/environment/dbfs/create-job.py | 48 +++ tests/environment/dbfs/mounts.py | 34 ++ tests/environment/requirements.txt | 29 ++ tests/environment/sources/adlsg2.bicep | 30 ++ tests/environment/sources/sql.bicep | 22 + tests/environment/sources/sqlserver.bicep | 21 + tests/environment/sources/synapse.bicep | 75 ++++ .../sparksubmit-test-def.json | 2 +- .../integration/jobdefs/hive3-tests-def.json | 178 ++++----- .../jobdefs/hive3-tests-expectations.json | 12 +- .../integration/jobdefs/jarjob-test-def.json | 48 ++- .../jobdefs/jarjob-test-expectations.json | 2 +- .../jobdefs/pythonscript-test-def.json | 72 ++-- .../pythonscript-test-expectations.json | 2 +- .../jobdefs/pythonwheel-test-def.json | 46 +-- .../pythonwheel-test-expectations.json | 2 +- .../integration/jobdefs/spark2-tests-def.json | 116 +++--- .../jobdefs/spark2-tests-expectations.json | 6 +- .../integration/jobdefs/spark3-tests-def.json | 376 +++++++++--------- .../jobdefs/spark3-tests-expectations.json | 28 +- .../wasbs-in-wasbs-out-with-param-def.json | 25 -- ...-in-wasbs-out-with-param-expectations.json | 6 - tests/integration/run-test.sh | 4 +- .../app/src/main/java/SparkApp/Basic/App.java | 2 +- .../notebooks/abfss-in-abfss-out-oauth.scala | 6 +- .../notebooks/abfss-in-abfss-out-root.scala | 2 +- .../notebooks/abfss-in-abfss-out.scala | 2 +- ...abfss-in-hive+notmgd+saveAsTable-out.scala | 2 +- .../abfss-in-hive+saveAsTable-out.scala | 2 +- .../notebooks/azuresql-in-azuresql-out.scala | 8 +- .../notebooks/call-via-adf-spark2.scala | 2 +- .../notebooks/call-via-adf-spark3.scala | 2 +- .../notebooks/delta-in-delta-merge.scala | 8 +- .../notebooks/delta-in-delta-out-abfss.scala | 8 +- .../notebooks/delta-in-delta-out-fs.scala | 8 +- .../notebooks/delta-in-delta-out-mnt.scala | 8 +- .../hive+abfss-in-hive+abfss-out-insert.py | 44 +- ...ault-in-hive+mgd+not+default-out-insert.py | 37 +- .../notebooks/hive-in-hive-out-insert.py | 27 +- .../notebooks/intermix-languages.scala | 5 +- .../spark-apps/notebooks/mnt-in-mnt-out.scala | 2 +- .../notebooks/name-with-periods.scala | 2 +- .../spark-apps/notebooks/nested-child.scala | 2 +- .../spark-sql-table-in-abfss-out.scala | 2 +- .../notebooks/synapse-in-synapse-out.scala | 20 +- .../notebooks/synapse-in-wasbs-out.scala | 22 +- .../synapse-wasbs-in-synapse-out.scala | 22 +- .../wasbs-in-wasbs-out-with-param.py | 39 -- .../notebooks/wasbs-in-wasbs-out.scala | 8 +- .../spark-apps/pythonscript/pythonscript.json | 2 +- .../spark-apps/sparksubmit/sparksubmit.json | 2 +- .../abfssInAbfssOut/abfssintest/main.py | 2 +- .../wheeljobs/abfssInAbfssOut/db-job-def.json | 18 - 60 files changed, 1486 insertions(+), 709 deletions(-) create mode 100644 tests/environment/README.md create mode 100644 tests/environment/config.json create mode 100644 tests/environment/datasets/azsql.sql create mode 100644 tests/environment/datasets/make-data.py create mode 100644 tests/environment/datasets/sqlpool.sql create mode 100644 tests/environment/dbfs/create-job.py create mode 100644 tests/environment/dbfs/mounts.py create mode 100644 tests/environment/requirements.txt create mode 100644 tests/environment/sources/adlsg2.bicep create mode 100644 tests/environment/sources/sql.bicep create mode 100644 tests/environment/sources/sqlserver.bicep create mode 100644 tests/environment/sources/synapse.bicep delete mode 100644 tests/integration/jobdefs/wasbs-in-wasbs-out-with-param-def.json delete mode 100644 tests/integration/jobdefs/wasbs-in-wasbs-out-with-param-expectations.json delete mode 100644 tests/integration/spark-apps/notebooks/wasbs-in-wasbs-out-with-param.py delete mode 100644 tests/integration/spark-apps/wheeljobs/abfssInAbfssOut/db-job-def.json diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 1de752d..34b8b97 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -85,18 +85,19 @@ jobs: name: FunctionZip path: ./artifacts - - name: Azure Functions Action + - name: Deploy Azure Function to Integration Env uses: Azure/functions-action@v1.4.6 with: app-name: ${{ secrets.INT_FUNC_NAME }} package: ./artifacts/FunctionZip.zip publish-profile: ${{ secrets.INT_PUBLISH_PROFILE }} - - uses: azure/login@v1 + - name: Azure Login + uses: azure/login@v1 with: creds: ${{ secrets.INT_AZ_CLI_CREDENTIALS }} - - name: Azure CLI script + - name: Compare and Update App Settings on Deployed Function uses: azure/CLI@v1 with: azcliversion: 2.34.1 @@ -108,7 +109,7 @@ jobs: # Start up Synapse Pool and Execute Tests - name: Start Integration Synapse SQL Pool - run: source tests/integration/manage-sql-pool.sh start ${{ secrets.INT_SUBSCRIPTION_ID }} ${{ secrets.INT_RG_NAME }} ${{ secrets.INT_SYNAPSE_WKSP_NAME }} ${{ secrets.INT_SYNAPSE_SQLPOOL_NAME }} + run: source tests/integration/manage-sql-pool.sh start ${{ secrets.INT_SUBSCRIPTION_ID }} ${{ secrets.INT_SYNAPSE_SQLPOOL_RG_NAME }} ${{ secrets.INT_SYNAPSE_WKSP_NAME }} ${{ secrets.INT_SYNAPSE_SQLPOOL_NAME }} env: AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} @@ -124,6 +125,10 @@ jobs: token = ${{ secrets.INT_DATABRICKS_ACCESS_TOKEN }}" > ./config.ini export DATABRICKS_CONFIG_FILE=./config.ini + - name: Confirm Databricks CLI is configured + run: databricks clusters spark-versions + env: + DATABRICKS_CONFIG_FILE: ./config.ini - name: Cleanup Integration Environment run: python ./tests/integration/runner.py --cleanup --dontwait None None None @@ -144,7 +149,7 @@ jobs: DATABRICKS_CONFIG_FILE: ./config.ini - name: Stop Integration Synapse SQL Pool - run: source tests/integration/manage-sql-pool.sh stop ${{ secrets.INT_SUBSCRIPTION_ID }} ${{ secrets.INT_RG_NAME }} ${{ secrets.INT_SYNAPSE_WKSP_NAME }} ${{ secrets.INT_SYNAPSE_SQLPOOL_NAME }} + run: source tests/integration/manage-sql-pool.sh stop ${{ secrets.INT_SUBSCRIPTION_ID }} ${{ secrets.INT_SYNAPSE_SQLPOOL_RG_NAME }} ${{ secrets.INT_SYNAPSE_WKSP_NAME }} ${{ secrets.INT_SYNAPSE_SQLPOOL_NAME }} env: AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} @@ -172,25 +177,3 @@ jobs: with: artifacts: ~/artifacts/FunctionZip.zip token: ${{ secrets.GITHUB_TOKEN }} - - deployProductionEnvironment: - name: Release to Production Environment - needs: [createRelease] - runs-on: ubuntu-latest - environment: - name: Production - steps: - - uses: actions/checkout@v3 - - - name: Download Artifact - uses: actions/download-artifact@v3 - with: - name: FunctionZip - path: ./artifacts - - - name: Azure Functions Action - uses: Azure/functions-action@v1.4.6 - with: - app-name: ${{ secrets.FUNC_NAME }} - package: ./artifacts/FunctionZip.zip - publish-profile: ${{ secrets.PUBLISH_PROFILE }} diff --git a/.gitignore b/.gitignore index 0c3849b..9343284 100644 --- a/.gitignore +++ b/.gitignore @@ -161,3 +161,4 @@ build # Ignore local settings localsettingsdutils.py +*.ini diff --git a/tests/environment/README.md b/tests/environment/README.md new file mode 100644 index 0000000..7ae4daa --- /dev/null +++ b/tests/environment/README.md @@ -0,0 +1,122 @@ +# Deploying the Test Environment + +## Deploying the Connector + +## Deploying the Data Sources + +``` +az deployment group create \ +--template-file ./tests/environment/sources/adlsg2.bicep \ +--resource-group db2pvsasources + +``` + +## Manual Steps + +Create a config.ini file: + +```ini +databricks_workspace_host_id = adb-workspace.id +databricks_personal_access_token = PERSONAL_ACCESS_TOKEN +databricks_spark3_cluster = CLUSTER_ID +databricks_spark2_cluster = CLUSTER_ID +``` + +Assign Service Principal Storage Blob Data Contributor to the main ADLS G2 instance + +Add Service Principal as user in Databricks. + +Enable mount points with `./tests/environment/dbfs/mounts.py` + +Add Key Vault Secrets + * `tenant-id` + * `storage-service-key` + * `azuresql-username` + * `azuresql-password` + * `azuresql-jdbc-conn-str` should be of the form `jdbc:sqlserver://SERVER_NAME.database.windows.net:1433;database=DATABASE_NAME;encrypt=true;trustServerCertificate=false;hostNameInCertificate=*.database.windows.net;loginTimeout=30;` + * `synapse-storage-key` + * `synapse-query-username` + * `synapse-query-password` +* Update SQL Db and Synapse Server with AAD Admin +* Add Service Principal for Databricks to connect to SQL sources + +Set the following system environments: + +* `SYNAPSE_SERVICE_NAME` +* `STORAGE_SERVICE_NAME` +* `SYNAPSE_STORAGE_SERVICE_NAME` + +Upload notebooks in `./tests/integration/spark-apps/notebooks/` to dbfs' `/Shared/examples/` + +* Manually for now. TODO: Automate this in Python + +Compile the following apps and upload them to `/dbfs/FileStore/testcases/` + +* `./tests/integration/spark-apps/jarjobs/abfssInAbfssOut/` with `./gradlew build` +* `./tests/integration/spark-apps/pythonscript/pythonscript.py` by just uploading. +* `./tests/integration/spark-apps/wheeljobs/abfssintest/` with `python -m build` + +Upload the job definitions using the python script `python .\tests\environment\dbfs\create-job.py` + +## Github Actions + +* AZURE_CLIENT_ID +* AZURE_CLIENT_SECRET +* AZURE_TENANT_ID +* INT_AZ_CLI_CREDENTIALS + ```json + { + "clientId": "xxxx", + "clientSecret": "yyyy", + "subscriptionId": "zzzz", + "tenantId": "μμμμ", + "activeDirectoryEndpointUrl": "https://login.microsoftonline.com", + "resourceManagerEndpointUrl": "https://management.azure.com/", + "activeDirectoryGraphResourceId": "https://graph.windows.net/", + "sqlManagementEndpointUrl": "https://management.core.windows.net:8443/", + "galleryEndpointUrl": "https://gallery.azure.com/", + "managementEndpointUrl": "https://management.core.windows.net/" + } + ``` +* INT_DATABRICKS_ACCESS_TOKEN +* INT_DATABRICKS_WKSP_ID: adb-xxxx.y +* INT_FUNC_NAME +* INT_PUBLISH_PROFILE from the Azure Function's publish profile XML +* INT_PURVIEW_NAME +* INT_RG_NAME +* INT_SUBSCRIPTION_ID +* INT_SYNAPSE_SQLPOOL_NAME +* INT_SYNAPSE_WKSP_NAME +* INT_SYNAPSE_WKSP_NAME + +## config.json + +```json +{ + "datasets":{ + "datasetName": { + "schema": [ + "field1", + "field2" + ], + "data": [ + [ + "val1", + "val2" + ] + ] + } + }, + "jobs": { + "job-name": [ + [ + ("storage"|"sql"|"noop"), + ("csv"|"delta"|"azuresql"|"synapse"), + "rawdata/testcase/one/", + "exampleInputA" + ] + ] + } +} + +``` diff --git a/tests/environment/config.json b/tests/environment/config.json new file mode 100644 index 0000000..79e4721 --- /dev/null +++ b/tests/environment/config.json @@ -0,0 +1,366 @@ +{ + "dataset": { + "exampleInputA": { + "schema": [ + "id", + "postalCode", + "street" + ], + "data": [ + [ + 1, + "555", + "742 Evergreen Terrace" + ] + ] + }, + "exampleInputB": { + "schema": [ + "id", + "city", + "stateAbbreviation" + ], + "data": [ + [ + 1, + "Springfield", + "??" + ] + ] + } + }, + "jobs": { + "jarjobs-abfssInAbfssOut": [ + [ + "storage", + "csv", + "rawdata/testcase/eighteen/", + "exampleInputA" + ] + ], + "pythonscript-pythonscript.py": [ + [ + "storage", + "csv", + "rawdata/testcase/twenty/", + "exampleInputA" + ] + ], + "wheeljobs-abfssintest": [ + [ + "storage", + "csv", + "rawdata/testcase/seventeen/", + "exampleInputA" + ], + [ + "storage", + "csv", + "rawdata/testcase/seventeen/", + "exampleInputB" + ] + ], + "abfss-in-abfss-out-oauth.scala": [ + [ + "storage", + "csv", + "rawdata/testcase/two/", + "exampleInputA" + ], + [ + "storage", + "csv", + "rawdata/testcase/two/", + "exampleInputB" + ] + ], + "abfss-in-abfss-out-root.scala": [ + [ + "storage", + "csv", + "rawdata/testcase/three/", + "exampleInputA" + ], + [ + "storage", + "csv", + "rawdata/testcase/three/", + "exampleInputB" + ] + ], + "abfss-in-abfss-out.scala": [ + [ + "storage", + "csv", + "rawdata/testcase/one/", + "exampleInputA" + ], + [ + "storage", + "csv", + "rawdata/testcase/one/", + "exampleInputB" + ] + ], + "abfss-in-hive+notmgd+saveAsTable-out.scala": [ + [ + "storage", + "delta", + "rawdata/testcase/abfss-in-hive+notmgd+saveAsTable-out/", + "exampleInputA" + ] + ], + "abfss-in-hive+saveAsTable-out.scala": [ + [ + "storage", + "delta", + "rawdata/testcase/abfss-in-hive+saveAsTable-out/", + "exampleInputA" + ] + ], + "azuresql-in-azuresql-out.scala": [ + [ + "azuresql", + "table", + "dbo", + "exampleInputA" + ], + [ + "azuresql", + "table", + "dbo", + "exampleInputB" + ], + [ + "azuresql", + "table", + "dbo.exampleInputC" + ], + [ + "azuresql", + "table", + "dbo.exampleOutput" + ] + ], + "call-via-adf-spark2.scala": [ + [ + "storage", + "csv", + "rawdata/testcase/thirteen/", + "exampleInputA" + ], + [ + "storage", + "csv", + "rawdata/testcase/thirteen/", + "exampleInputB" + ] + ], + "call-via-adf-spark3.scala": [ + [ + "storage", + "csv", + "rawdata/testcase/fourteen/", + "exampleInputA" + ], + [ + "storage", + "csv", + "rawdata/testcase/fourteen/", + "exampleInputB" + ] + ], + "delta-in-delta-merge.scala": [ + [ + "storage", + "delta", + "rawdata/testcase/sixteen/", + "exampleInputA" + ], + [ + "storage", + "delta", + "rawdata/testcase/sixteen/", + "exampleInputB" + ] + ], + "delta-in-delta-out-abfss.scala": [ + [ + "storage", + "delta", + "rawdata/testcase/four/", + "exampleInputA" + ], + [ + "storage", + "delta", + "rawdata/testcase/four/", + "exampleInputB" + ] + ], + "delta-in-delta-out-fs.scala": [ + [ + "storage", + "delta", + "rawdata/testcase/five/", + "exampleInputA" + ], + [ + "storage", + "delta", + "rawdata/testcase/five/", + "exampleInputB" + ] + ], + "delta-in-delta-out-mnt.scala": [ + [ + "storage", + "delta", + "rawdata/testcase/six/", + "exampleInputA" + ], + [ + "storage", + "delta", + "rawdata/testcase/six/", + "exampleInputB" + ] + ], + "hive-in-hive-out-insert.py": [ + [ + "noop" + ] + ], + "hive+abfss-in-hive+abfss-out-insert.py": [ + [ + "storage", + "delta", + "rawdata/testcase/twentyone/", + "exampleInputA" + ] + ], + "hive+mgd+not+default-in-hive+mgd+not+default-out-insert.py": [ + [ + "noop" + ] + ], + "hive+mnt-in-hive+mnt-out-insert.py": [ + [ + "noop" + ] + ], + "intermix-languages.scala": [ + [ + "storage", + "csv", + "rawdata/testcase/fifteen/", + "exampleInputA" + ], + [ + "storage", + "csv", + "rawdata/testcase/fifteen/", + "exampleInputB" + ] + ], + "mnt-in-mnt-out.scala": [ + [ + "storage", + "csv", + "rawdata/testcase/seven/", + "exampleInputA" + ], + [ + "storage", + "csv", + "rawdata/testcase/seven/", + "exampleInputB" + ] + ], + "name-with-periods.scala": [ + [ + "storage", + "csv", + "rawdata/testcase/namewithperiods/", + "exampleInputA" + ] + ], + "nested-child.scala": [ + [ + "storage", + "csv", + "rawdata/testcase/eight/", + "exampleInputA" + ] + ], + "nested-parent.scala": [ + [ + "noop" + ] + ], + "spark-sql-table-in-abfss-out.scala": [ + [ + "storage", + "csv", + "rawdata/testcase/nine/", + "exampleInputB" + ] + ], + "synapse-in-synapse-out.scala": [ + [ + "synapse", + "table", + "dbo", + "exampleInputA" + ], + [ + "synapse", + "table", + "Sales", + "Region" + ] + ], + "synapse-in-wasbs-out.scala": [ + [ + "synapse", + "table", + "dbo", + "exampleInputA" + ], + [ + "synapse", + "table", + "dbo", + "exampleInputB" + ] + ], + "synapse-wasbs-in-synapse-out.scala": [ + [ + "synapse", + "table", + "dbo", + "exampleInputA" + ], + [ + "storage", + "csv", + "rawdata/testcase/eleven/", + "exampleInputA" + ] + ], + "wasbs-in-wasbs-out.scala": [ + [ + "storage", + "csv", + "rawdata/testcase/wasinwasout/", + "exampleInputA" + ], + [ + "storage", + "csv", + "rawdata/testcase/wasinwasout/", + "exampleInputB" + ] + ] + } +} \ No newline at end of file diff --git a/tests/environment/datasets/azsql.sql b/tests/environment/datasets/azsql.sql new file mode 100644 index 0000000..8eeba28 --- /dev/null +++ b/tests/environment/datasets/azsql.sql @@ -0,0 +1,27 @@ +CREATE SCHEMA nondbo + +CREATE TABLE nondbo.exampleInputC ( +id int +,cityPopulation int +) + +CREATE TABLE dbo.exampleInputB ( +id int +,city varchar(30) +,stateAbbreviation varchar(2) +) + +CREATE TABLE dbo.exampleInputA ( +id int +,postalcode varchar(5) +,street varchar(50) +) + +INSERT INTO nondbo.exampleInputC(id, cityPopulation) +VALUES(1, 1000) + +INSERT INTO dbo.exampleInputB(id, city, stateAbbreviation) +VALUES(1, 'Springfield', '??') + +INSERT INTO dbo.exampleInputA(id, postalcode, street) +VALUES(1, '55555', '742 Evergreen Terrace') diff --git a/tests/environment/datasets/make-data.py b/tests/environment/datasets/make-data.py new file mode 100644 index 0000000..21fb1a0 --- /dev/null +++ b/tests/environment/datasets/make-data.py @@ -0,0 +1,112 @@ +import argparse +import configparser +from io import BytesIO +import json +import pathlib +import re + + +from azure.identity import DefaultAzureCredential +from azure.storage.blob import BlobServiceClient, BlobClient + +def make_or_get_connection_client(connection_string, cached_connections, **kwargs): + if connection_string in cached_connections: + return cached_connections[connection_string] + + elif re.search(r'EndpointSuffix=', connection_string): # Is Blob + _client = BlobServiceClient.from_connection_string(connection_string) + cached_connections[connection_string] = _client + return _client + else: + raise NotImplementedError("Connection String not supported") + + +def make_and_upload_data(client, storage_path, dataset_name, storage_format, data): + if isinstance(client, BlobServiceClient): + blob_full_path = pathlib.Path(storage_path) + container = blob_full_path.parts[0] + blob_relative_path = '/'.join(list(blob_full_path.parts[1:])+[dataset_name, dataset_name+"."+storage_format]) + + _blob_client = client.get_blob_client(container, blob_relative_path) + blob_stream = BytesIO() + with blob_stream as fp: + for row in data["data"]: + fp.write(bytes(','.join(str(r) for r in row), encoding="utf-8")) + fp.seek(0) + _blob_client.upload_blob(blob_stream.read(), blob_type="BlockBlob", overwrite=True) + else: + raise NotImplementedError(f"{type(client)} not supported") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--test_case", "-t", type=str, action="append", help="Name of the test case(s) to be deployed. If not specified, upload all datasets") + parser.add_argument("--config", type=str, help="Path to the json config file", default="./tests/environment/config.json") + parser.add_argument("--ini", type=str, help="Path to the ini config file", default="./tests/environment/config.ini") + args = parser.parse_args() + + # Datasets + ## CSV + ## Parquet + ## Delta + ## SQL + ## COSMOS + ## Kusto + + # Load Test Cases + ## jobs and dataset + _connections = configparser.ConfigParser() + _connections.read(args.ini) + + with open(args.config, 'r') as fp: + _config = json.load(fp) + TEST_JOBS = _config["jobs"] + TEST_DATASET = _config["dataset"] + + # Filter based on test cases provided + if args.test_case: + print(args.test_case) + jobs_to_build_data = {k:v for k,v in TEST_JOBS.items() if k in args.test_case} + else: + jobs_to_build_data = TEST_JOBS + + + # Make the data only one time + cached_data = {} + # Make the connections only one time + cached_connections = {} + # Iterate over every job and build the dataset + for job_name, dataset_def in jobs_to_build_data.items(): + if len(dataset_def) == 0 or dataset_def[0] == ["noop"]: + print(f"{job_name}: skipped") + continue + + for dataset in dataset_def: + _connection_name = dataset[0] + _storage_format = dataset[1] + _storage_path = dataset[2] + _dataset_name = dataset[3] + + print(f"{job_name}: {_storage_path}") + + _connection_string = _connections["DEFAULT"][_connection_name+"_connection_string"] + + _client = make_or_get_connection_client(_connection_string, cached_connections) + + _data = TEST_DATASET[_dataset_name] + + make_and_upload_data( + _client, + _storage_path, + _dataset_name, + _storage_format, + _data + ) + + + + # Check which storage engine is necessary + # Check what format the data will be stored in + # Check the pat + + \ No newline at end of file diff --git a/tests/environment/datasets/sqlpool.sql b/tests/environment/datasets/sqlpool.sql new file mode 100644 index 0000000..3b357c2 --- /dev/null +++ b/tests/environment/datasets/sqlpool.sql @@ -0,0 +1,30 @@ +CREATE MASTER KEY ENCRYPTION BY PASSWORD = 'xxxx' ; /* Necessary for Synapse External tables */ +CREATE SCHEMA Sales + +CREATE TABLE Sales.Region ( +id int +,regionId int +) + +CREATE TABLE dbo.exampleInputB ( +id int +,city varchar(30) +,stateAbbreviation varchar(2) +) + +CREATE TABLE dbo.exampleInputA ( +id int +,postalcode varchar(5) +,street varchar(50) +) + + + +INSERT INTO Sales.Region(id, regionId) +VALUES(1, 1000) + +INSERT INTO dbo.exampleInputB(id, city, stateAbbreviation) +VALUES(1, 'Springfield', '??') + +INSERT INTO dbo.exampleInputA(id, postalcode, street) +VALUES(1, '55555', '742 Evergreen Terrace') diff --git a/tests/environment/dbfs/create-job.py b/tests/environment/dbfs/create-job.py new file mode 100644 index 0000000..3e7f7be --- /dev/null +++ b/tests/environment/dbfs/create-job.py @@ -0,0 +1,48 @@ +# https://learn.microsoft.com/en-us/azure/databricks/dev-tools/api/latest/workspace#--import +import argparse +import configparser +import json +import os + +import requests + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--folder", default="./tests/integration/jobdefs") + parser.add_argument("--ini", default="./tests/environment/config.ini") + args = parser.parse_args() + + cfp = configparser.ConfigParser() + + cfp.read(args.ini) + db_host_id = cfp["DEFAULT"]["databricks_workspace_host_id"] + db_pat = cfp["DEFAULT"]["databricks_personal_access_token"] + + JOB_URL = f"https://{db_host_id}.azuredatabricks.net/api/2.1/jobs/create" + for job_def in os.listdir(args.folder): + if not job_def.endswith("-def.json"): + continue + + print(job_def) + with open(os.path.join(args.folder, job_def), 'r') as fp: + job_json = json.load(fp) + + job_str = json.dumps(job_json) + if job_def.startswith("spark2"): + job_str = job_str.replace("", cfp["DEFAULT"]["databricks_spark2_cluster"]) + else: + job_str = job_str.replace("", cfp["DEFAULT"]["databricks_spark3_cluster"]) + + job_json_to_submit = json.loads(job_str) + + resp = requests.post( + url=JOB_URL, + json=job_json_to_submit, + headers={ + "Authorization": f"Bearer {db_pat}" + } + ) + print(resp.content) + + diff --git a/tests/environment/dbfs/mounts.py b/tests/environment/dbfs/mounts.py new file mode 100644 index 0000000..d03a942 --- /dev/null +++ b/tests/environment/dbfs/mounts.py @@ -0,0 +1,34 @@ +# Databricks notebook source +import os + +storage_acct_name = os.environ.get("STORAGE_SERVICE_NAME") +configs = {"fs.azure.account.auth.type": "OAuth", + "fs.azure.account.oauth.provider.type": "org.apache.hadoop.fs.azurebfs.oauth2.ClientCredsTokenProvider", + "fs.azure.account.oauth2.client.id": dbutils.secrets.get("purview-to-adb-kv", 'clientIdKey'), + "fs.azure.account.oauth2.client.secret": dbutils.secrets.get("purview-to-adb-kv", 'clientSecretKey'), + "fs.azure.account.oauth2.client.endpoint": f"https://login.microsoftonline.com/{dbutils.secrets.get('purview-to-adb-kv', 'tenant-id')}/oauth2/token"} + +# COMMAND ---------- + +# Optionally, you can add to the source URI of your mount point. +try: + dbutils.fs.mount( + source = f"abfss://rawdata@{storage_acct_name}.dfs.core.windows.net/", + mount_point = "/mnt/rawdata", + extra_configs = configs) +except Exception as e: + print(e) + +# COMMAND ---------- + +try: + dbutils.fs.mount( + source = f"abfss://outputdata@{storage_acct_name}.dfs.core.windows.net/", + mount_point = "/mnt/outputdata", + extra_configs = configs) +except Exception as e: + print(e) + +# COMMAND ---------- + + diff --git a/tests/environment/requirements.txt b/tests/environment/requirements.txt new file mode 100644 index 0000000..30cf059 --- /dev/null +++ b/tests/environment/requirements.txt @@ -0,0 +1,29 @@ +azure-core==1.26.1 +azure-identity==1.12.0 +azure-storage-blob==12.14.1 +build==0.9.0 +certifi==2022.12.7 +cffi==1.15.1 +charset-normalizer==2.1.1 +colorama==0.4.6 +cryptography==38.0.4 +idna==3.4 +importlib-metadata==5.1.0 +isodate==0.6.1 +msal==1.20.0 +msal-extensions==1.0.0 +msrest==0.7.1 +oauthlib==3.2.2 +packaging==22.0 +pep517==0.13.0 +portalocker==2.6.0 +pycparser==2.21 +PyJWT==2.6.0 +pywin32==305 +requests==2.28.1 +requests-oauthlib==1.3.1 +six==1.16.0 +tomli==2.0.1 +typing_extensions==4.4.0 +urllib3==1.26.13 +zipp==3.11.0 diff --git a/tests/environment/sources/adlsg2.bicep b/tests/environment/sources/adlsg2.bicep new file mode 100644 index 0000000..de69883 --- /dev/null +++ b/tests/environment/sources/adlsg2.bicep @@ -0,0 +1,30 @@ +@description('Location of the data factory.') +param location string = resourceGroup().location + +@description('Name of the Azure storage account that contains the input/output data.') +param storageAccountName string = 'storage${uniqueString(resourceGroup().id)}' + +resource storageAccount 'Microsoft.Storage/storageAccounts@2021-08-01' = { + name: storageAccountName + location: location + sku: { + name: 'Standard_LRS' + } + kind: 'StorageV2' + properties:{ + isHnsEnabled: true + } + +} + +resource rawdataContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2021-08-01' = { + name: '${storageAccount.name}/default/rawdata' +} + +resource writeToRootContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2021-08-01' = { + name: '${storageAccount.name}/default/writetoroot' +} + +resource outputdataContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2021-08-01' = { + name: '${storageAccount.name}/default/outputdata' +} diff --git a/tests/environment/sources/sql.bicep b/tests/environment/sources/sql.bicep new file mode 100644 index 0000000..af14c46 --- /dev/null +++ b/tests/environment/sources/sql.bicep @@ -0,0 +1,22 @@ +@description('The name of the SQL logical server.') +param serverName string = uniqueString('sql', resourceGroup().id) + +@description('The name of the SQL Database.') +param sqlDBName string = 'SampleDB' + +@description('Location for all resources.') +param location string = resourceGroup().location + +resource sqlServer 'Microsoft.Sql/servers@2022-05-01-preview' existing = { + name: serverName +} + +resource sqlDB 'Microsoft.Sql/servers/databases@2022-05-01-preview' = { + parent: sqlServer + name: sqlDBName + location: location + sku: { + name: 'Basic' + tier: 'Basic' + } +} diff --git a/tests/environment/sources/sqlserver.bicep b/tests/environment/sources/sqlserver.bicep new file mode 100644 index 0000000..6d503a1 --- /dev/null +++ b/tests/environment/sources/sqlserver.bicep @@ -0,0 +1,21 @@ +@description('The name of the SQL logical server.') +param serverName string = uniqueString('sql', resourceGroup().id) + +@description('Location for all resources.') +param location string = resourceGroup().location + +@description('The administrator username of the SQL logical server.') +param administratorLogin string + +@description('The administrator password of the SQL logical server.') +@secure() +param administratorLoginPassword string + +resource sqlServer 'Microsoft.Sql/servers@2022-05-01-preview' = { + name: serverName + location: location + properties: { + administratorLogin: administratorLogin + administratorLoginPassword: administratorLoginPassword + } +} diff --git a/tests/environment/sources/synapse.bicep b/tests/environment/sources/synapse.bicep new file mode 100644 index 0000000..916ac65 --- /dev/null +++ b/tests/environment/sources/synapse.bicep @@ -0,0 +1,75 @@ +@description('The Synapse Workspace name.') +param workspaceName string = uniqueString('synwksp', resourceGroup().id) + +@description('Location for all resources.') +param location string = resourceGroup().location + +@description('The administrator username of the SQL logical server.') +@secure() +param administratorLogin string + +@description('The administrator password of the SQL logical server.') +@secure() +param administratorLoginPassword string + +var supportingStorageName = '${workspaceName}sa' + +resource storageAccount 'Microsoft.Storage/storageAccounts@2021-08-01' = { + name: supportingStorageName + location: location + sku: { + name: 'Standard_LRS' + } + kind: 'StorageV2' + properties:{ + isHnsEnabled: true + } + +} + +resource rawdataContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2021-08-01' = { + name: '${storageAccount.name}/default/defaultcontainer' +} + +resource tempContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2021-08-01' = { + name: '${storageAccount.name}/default/temp' +} + +resource synapseWorkspace 'Microsoft.Synapse/workspaces@2021-06-01' = { + name: workspaceName + location: location + identity: { + type: 'SystemAssigned' + } + properties: { + azureADOnlyAuthentication: false + defaultDataLakeStorage: { + accountUrl: 'https://${storageAccount.name}.dfs.core.windows.net' + createManagedPrivateEndpoint: false + filesystem: 'synapsefs' + resourceId: resourceId('Microsoft.Storage/storageAccounts/', storageAccount.name) + } + managedResourceGroupName: '${workspaceName}rg' + + publicNetworkAccess: 'Enabled' + sqlAdministratorLogin: administratorLogin + sqlAdministratorLoginPassword: administratorLoginPassword + trustedServiceBypassEnabled: true + } +} + +resource symbolicname 'Microsoft.Synapse/workspaces/sqlPools@2021-06-01' = { + name: 'sqlpool1' + location: location + sku: { + name: 'DW100c' + capacity: 0 + } + parent: synapseWorkspace + properties: { + collation: 'SQL_Latin1_General_CP1_CI_AS' + createMode: 'Default' + + storageAccountType: 'LRS' + } +} diff --git a/tests/integration/jobdefs-inactive/sparksubmit-test-def.json b/tests/integration/jobdefs-inactive/sparksubmit-test-def.json index 8400a9e..508c609 100644 --- a/tests/integration/jobdefs-inactive/sparksubmit-test-def.json +++ b/tests/integration/jobdefs-inactive/sparksubmit-test-def.json @@ -19,7 +19,7 @@ "cluster_name": "", "spark_version": "9.1.x-scala2.12", "spark_conf": { - "spark.openlineage.url.param.code": "{{secrets/purview-to-adb-scope/Ol-Output-Api-Key}}", + "spark.openlineage.url.param.code": "{{secrets/purview-to-adb-kv/Ol-Output-Api-Key}}", "spark.openlineage.host": "https://.azurewebsites.net", "spark.openlineage.namespace": "#ABC123", "spark.openlineage.version": "1" diff --git a/tests/integration/jobdefs/hive3-tests-def.json b/tests/integration/jobdefs/hive3-tests-def.json index 25aea81..8fb9996 100644 --- a/tests/integration/jobdefs/hive3-tests-def.json +++ b/tests/integration/jobdefs/hive3-tests-def.json @@ -1,98 +1,96 @@ { - "settings": { - "name": "hive3-tests", - "email_notifications": { - "no_alert_for_skipped_runs": false + "name": "hive3-tests", + "email_notifications": { + "no_alert_for_skipped_runs": false + }, + "timeout_seconds": 0, + "max_concurrent_runs": 1, + "tasks": [ + { + "task_key": "hive-in-hive-out-insert", + "notebook_task": { + "notebook_path": "/Shared/examples/hive-in-hive-out-insert", + "source": "WORKSPACE" + }, + "existing_cluster_id": "", + "timeout_seconds": 0, + "email_notifications": {} }, - "timeout_seconds": 0, - "max_concurrent_runs": 1, - "tasks": [ - { - "task_key": "hive-in-hive-out-insert", - "notebook_task": { - "notebook_path": "/Shared/examples/hive-in-hive-out-insert", - "source": "WORKSPACE" - }, - "existing_cluster_id": "", - "timeout_seconds": 0, - "email_notifications": {} + { + "task_key": "hive_abfss-in-hive_abfss-out-insert", + "depends_on": [ + { + "task_key": "hive-in-hive-out-insert" + } + ], + "notebook_task": { + "notebook_path": "/Shared/examples/hive+abfss-in-hive+abfss-out-insert", + "source": "WORKSPACE" }, - { - "task_key": "hive_abfss-in-hive_abfss-out-insert", - "depends_on": [ - { - "task_key": "hive-in-hive-out-insert" - } - ], - "notebook_task": { - "notebook_path": "/Shared/examples/hive+abfss-in-hive+abfss-out-insert", - "source": "WORKSPACE" - }, - "existing_cluster_id": "", - "timeout_seconds": 0, - "email_notifications": {} + "existing_cluster_id": "", + "timeout_seconds": 0, + "email_notifications": {} + }, + { + "task_key": "hive_mgd_not_default-in-hive_mgd_not_default-out-insert", + "depends_on": [ + { + "task_key": "hive_abfss-in-hive_abfss-out-insert" + } + ], + "notebook_task": { + "notebook_path": "/Shared/examples/hive+mgd+not+default-in-hive+mgd+not+default-out-insert", + "source": "WORKSPACE" }, - { - "task_key": "hive_mgd_not_default-in-hive_mgd_not_default-out-insert", - "depends_on": [ - { - "task_key": "hive_abfss-in-hive_abfss-out-insert" - } - ], - "notebook_task": { - "notebook_path": "/Shared/examples/hive+mgd+not+default-in-hive+mgd+not+default-out-insert", - "source": "WORKSPACE" - }, - "existing_cluster_id": "", - "timeout_seconds": 0, - "email_notifications": {} + "existing_cluster_id": "", + "timeout_seconds": 0, + "email_notifications": {} + }, + { + "task_key": "hive_mnt-in-hive_mnt-out-insert", + "depends_on": [ + { + "task_key": "hive_mgd_not_default-in-hive_mgd_not_default-out-insert" + } + ], + "notebook_task": { + "notebook_path": "/Shared/examples/hive+mnt-in-hive+mnt-out-insert", + "source": "WORKSPACE" }, - { - "task_key": "hive_mnt-in-hive_mnt-out-insert", - "depends_on": [ - { - "task_key": "hive_mgd_not_default-in-hive_mgd_not_default-out-insert" - } - ], - "notebook_task": { - "notebook_path": "/Shared/examples/hive+mnt-in-hive+mnt-out-insert", - "source": "WORKSPACE" - }, - "existing_cluster_id": "", - "timeout_seconds": 0, - "email_notifications": {} + "existing_cluster_id": "", + "timeout_seconds": 0, + "email_notifications": {} + }, + { + "task_key": "abfss-in-hive_notmgd_saveAsTable-out", + "depends_on": [ + { + "task_key": "hive_mnt-in-hive_mnt-out-insert" + } + ], + "notebook_task": { + "notebook_path": "/Shared/examples/abfss-in-hive+notmgd+saveAsTable-out", + "source": "WORKSPACE" }, - { - "task_key": "abfss-in-hive_notmgd_saveAsTable-out", - "depends_on": [ - { - "task_key": "hive_mnt-in-hive_mnt-out-insert" - } - ], - "notebook_task": { - "notebook_path": "/Shared/examples/abfss-in-hive+notmgd+saveAsTable-out", - "source": "WORKSPACE" - }, - "existing_cluster_id": "", - "timeout_seconds": 0, - "email_notifications": {} + "existing_cluster_id": "", + "timeout_seconds": 0, + "email_notifications": {} + }, + { + "task_key": "abfss-in-hive_saveAsTable-out", + "depends_on": [ + { + "task_key": "abfss-in-hive_notmgd_saveAsTable-out" + } + ], + "notebook_task": { + "notebook_path": "/Shared/examples/abfss-in-hive+saveAsTable-out", + "source": "WORKSPACE" }, - { - "task_key": "abfss-in-hive_saveAsTable-out", - "depends_on": [ - { - "task_key": "abfss-in-hive_notmgd_saveAsTable-out" - } - ], - "notebook_task": { - "notebook_path": "/Shared/examples/abfss-in-hive+saveAsTable-out", - "source": "WORKSPACE" - }, - "existing_cluster_id": "", - "timeout_seconds": 0, - "email_notifications": {} - } - ], - "format": "MULTI_TASK" - } + "existing_cluster_id": "", + "timeout_seconds": 0, + "email_notifications": {} + } + ], + "format": "MULTI_TASK" } \ No newline at end of file diff --git a/tests/integration/jobdefs/hive3-tests-expectations.json b/tests/integration/jobdefs/hive3-tests-expectations.json index bcd13dd..2d894ef 100644 --- a/tests/integration/jobdefs/hive3-tests-expectations.json +++ b/tests/integration/jobdefs/hive3-tests-expectations.json @@ -1,21 +1,21 @@ [ "databricks://.azuredatabricks.net/jobs/", "databricks://.azuredatabricks.net/jobs//tasks/hive-in-hive-out-insert", - "databricks://.azuredatabricks.net/jobs//tasks/hive-in-hive-out-insert/processes/9EA618584B76AF154FF5885F070A753F->8846F7679DA958CC91AEB2B6311C97D2", + "databricks://.azuredatabricks.net/jobs//tasks/hive-in-hive-out-insert/processes/2CE3088B4BAADD102F97D92B97F3AB79->E14B63BA5130659288E6B5DB7FC7F232", "databricks://.azuredatabricks.net/notebooks/Shared/examples/hive-in-hive-out-insert", "databricks://.azuredatabricks.net/jobs//tasks/abfss-in-hive_notmgd_saveAsTable-out", - "databricks://.azuredatabricks.net/jobs//tasks/abfss-in-hive_notmgd_saveAsTable-out/processes/DADD88BC04CD758A0D2EB06CE6F86431->C4D873C9A86F827AB135E541A4952BCD", + "databricks://.azuredatabricks.net/jobs//tasks/abfss-in-hive_notmgd_saveAsTable-out/processes/575BF7CF92625D35D6B9309C9561FE0A->43E1EB2B6E2B692F3AFDDDBD63762F41", "databricks://.azuredatabricks.net/notebooks/Shared/examples/abfss-in-hive+notmgd+saveAsTable-out", "databricks://.azuredatabricks.net/jobs//tasks/hive_abfss-in-hive_abfss-out-insert", - "databricks://.azuredatabricks.net/jobs//tasks/hive_abfss-in-hive_abfss-out-insert/processes/B5EA0788D2DFDD6724C9638A23C72530->C45E275909E82D362F516CB3DF62F01E", + "databricks://.azuredatabricks.net/jobs//tasks/hive_abfss-in-hive_abfss-out-insert/processes/0366CD2735F426A339DB69EBB00A6ABC->95F7EE6DC3AB03275F8FE27E98838D54", "databricks://.azuredatabricks.net/notebooks/Shared/examples/hive+abfss-in-hive+abfss-out-insert", "databricks://.azuredatabricks.net/jobs//tasks/hive_mgd_not_default-in-hive_mgd_not_default-out-insert", - "databricks://.azuredatabricks.net/jobs//tasks/hive_mgd_not_default-in-hive_mgd_not_default-out-insert/processes/ABC9D8E9383FFB2295BA21732E71BDE5->819BA9557FE05FAFFCD1E6C8C8B12239", + "databricks://.azuredatabricks.net/jobs//tasks/hive_mgd_not_default-in-hive_mgd_not_default-out-insert/processes/13AA3B6322616FF3E554C6A109EBAB5C->6FCA021CCAD4C906D5C29512215F86C9", "databricks://.azuredatabricks.net/notebooks/Shared/examples/hive+mgd+not+default-in-hive+mgd+not+default-out-insert", "databricks://.azuredatabricks.net/jobs//tasks/hive_mnt-in-hive_mnt-out-insert", - "databricks://.azuredatabricks.net/jobs//tasks/hive_mnt-in-hive_mnt-out-insert/processes/B5EA0788D2DFDD6724C9638A23C72530->C45E275909E82D362F516CB3DF62F01E", + "databricks://.azuredatabricks.net/jobs//tasks/hive_mnt-in-hive_mnt-out-insert/processes/0366CD2735F426A339DB69EBB00A6ABC->95F7EE6DC3AB03275F8FE27E98838D54", "databricks://.azuredatabricks.net/notebooks/Shared/examples/hive+mnt-in-hive+mnt-out-insert", "databricks://.azuredatabricks.net/jobs//tasks/abfss-in-hive_saveAsTable-out", - "databricks://.azuredatabricks.net/jobs//tasks/abfss-in-hive_saveAsTable-out/processes/B97ED17F23A32D631D1A53C1AE3A009A->7799B858F5B94A237932CDF9F987F8E0", + "databricks://.azuredatabricks.net/jobs//tasks/abfss-in-hive_saveAsTable-out/processes/D691CD0248B7A179C249AE6DA86A9A69->1073C801CC5F362F10F1CD1FFBA1972C", "databricks://.azuredatabricks.net/notebooks/Shared/examples/abfss-in-hive+saveAsTable-out" ] diff --git a/tests/integration/jobdefs/jarjob-test-def.json b/tests/integration/jobdefs/jarjob-test-def.json index 25dca17..4e66489 100644 --- a/tests/integration/jobdefs/jarjob-test-def.json +++ b/tests/integration/jobdefs/jarjob-test-def.json @@ -1,27 +1,25 @@ { - "settings": { - "name": "JarJob", - "email_notifications": { - "no_alert_for_skipped_runs": false - }, - "max_concurrent_runs": 1, - "tasks": [ - { - "task_key": "JarJob", - "spark_jar_task": { - "jar_uri": "", - "main_class_name": "SparkApp.Basic.App", - "run_as_repl": true - }, - "existing_cluster_id": "", - "libraries": [ - { - "jar": "dbfs:/FileStore/testcases/app.jar" - } - ], - "timeout_seconds": 0 - } - ], - "format": "MULTI_TASK" - } + "name": "JarJob", + "email_notifications": { + "no_alert_for_skipped_runs": false + }, + "max_concurrent_runs": 1, + "tasks": [ + { + "task_key": "JarJob", + "spark_jar_task": { + "jar_uri": "", + "main_class_name": "SparkApp.Basic.App", + "run_as_repl": true + }, + "existing_cluster_id": "", + "libraries": [ + { + "jar": "dbfs:/FileStore/testcases/app.jar" + } + ], + "timeout_seconds": 0 + } + ], + "format": "MULTI_TASK" } \ No newline at end of file diff --git a/tests/integration/jobdefs/jarjob-test-expectations.json b/tests/integration/jobdefs/jarjob-test-expectations.json index 304192f..06b31d8 100644 --- a/tests/integration/jobdefs/jarjob-test-expectations.json +++ b/tests/integration/jobdefs/jarjob-test-expectations.json @@ -1,5 +1,5 @@ [ "databricks://.azuredatabricks.net/jobs/", "databricks://.azuredatabricks.net/jobs//tasks/JarJob", - "databricks://.azuredatabricks.net/jobs//tasks/JarJob/processes/B4CFB465D62A3D282313EF88E9E4779C->2B1635731ED472A95FC7A53B61F02674" + "databricks://.azuredatabricks.net/jobs//tasks/JarJob/processes/CA1C8F378EABC4EF08062103C5D51CBE->560CF14B3818EF6B8FF5D0BC6AF7BCE9" ] diff --git a/tests/integration/jobdefs/pythonscript-test-def.json b/tests/integration/jobdefs/pythonscript-test-def.json index e9c1282..2358501 100644 --- a/tests/integration/jobdefs/pythonscript-test-def.json +++ b/tests/integration/jobdefs/pythonscript-test-def.json @@ -1,42 +1,40 @@ { - "settings": { - "name": "PythonScriptJob", - "email_notifications": {}, - "max_concurrent_runs": 1, - "tasks": [ - { - "task_key": "PythonScriptJob", - "spark_python_task": { - "python_file": "dbfs:/FileStore/testcases/pythonscript.py" + "name": "PythonScriptJob", + "email_notifications": {}, + "max_concurrent_runs": 1, + "tasks": [ + { + "task_key": "PythonScriptJob", + "spark_python_task": { + "python_file": "dbfs:/FileStore/testcases/pythonscript.py" + }, + "new_cluster": { + "spark_version": "9.1.x-scala2.12", + "spark_conf": { + "spark.openlineage.url.param.code": "{{secrets/purview-to-adb-kv/Ol-Output-Api-Key}}", + "spark.openlineage.host": "https://.azurewebsites.net", + "spark.openlineage.namespace": "#ABC123", + "spark.openlineage.version": "v1" }, - "new_cluster": { - "spark_version": "9.1.x-scala2.12", - "spark_conf": { - "spark.openlineage.url.param.code": "{{secrets/purview-to-adb-scope/Ol-Output-Api-Key}}", - "spark.openlineage.host": "https://.azurewebsites.net", - "spark.openlineage.namespace": "#ABC123", - "spark.openlineage.version": "1" - }, - "node_type_id": "Standard_DS3_v2", - "enable_elastic_disk": true, - "init_scripts": [ - { - "dbfs": { - "destination": "dbfs:/databricks/openlineagehardcoded/release-candidate.sh" - } + "node_type_id": "Standard_DS3_v2", + "enable_elastic_disk": true, + "init_scripts": [ + { + "dbfs": { + "destination": "dbfs:/databricks/openlineage/open-lineage-init-script.sh" } - ], - "azure_attributes": { - "availability": "ON_DEMAND_AZURE" - }, - "num_workers": 1 + } + ], + "azure_attributes": { + "availability": "ON_DEMAND_AZURE" }, - "max_retries": 1, - "min_retry_interval_millis": 0, - "retry_on_timeout": false, - "timeout_seconds": 3600 - } - ], - "format": "MULTI_TASK" - } + "num_workers": 1 + }, + "max_retries": 1, + "min_retry_interval_millis": 0, + "retry_on_timeout": false, + "timeout_seconds": 3600 + } + ], + "format": "MULTI_TASK" } \ No newline at end of file diff --git a/tests/integration/jobdefs/pythonscript-test-expectations.json b/tests/integration/jobdefs/pythonscript-test-expectations.json index 4fa1f04..077a08c 100644 --- a/tests/integration/jobdefs/pythonscript-test-expectations.json +++ b/tests/integration/jobdefs/pythonscript-test-expectations.json @@ -1,5 +1,5 @@ [ "databricks://.azuredatabricks.net/jobs/", "databricks://.azuredatabricks.net/jobs//tasks/PythonScriptJob", - "databricks://.azuredatabricks.net/jobs//tasks/PythonScriptJob/processes/EAEFBD6BB0CA1156256F42C7E3234487->FC65543BD0CEE9FB45BDD86AF033D876" + "databricks://.azuredatabricks.net/jobs//tasks/PythonScriptJob/processes/16D109EA9E8BC7329A7365311F917C1F->C862A921EE653ED2F3101026739FB936" ] \ No newline at end of file diff --git a/tests/integration/jobdefs/pythonwheel-test-def.json b/tests/integration/jobdefs/pythonwheel-test-def.json index adbd354..96196bc 100644 --- a/tests/integration/jobdefs/pythonwheel-test-def.json +++ b/tests/integration/jobdefs/pythonwheel-test-def.json @@ -1,26 +1,24 @@ { - "settings": { - "name": "WheelJob", - "email_notifications": { - "no_alert_for_skipped_runs": false - }, - "max_concurrent_runs": 1, - "tasks": [ - { - "task_key": "WheelJob", - "python_wheel_task": { - "package_name": "abfssintest", - "entry_point": "runapp" - }, - "existing_cluster_id": "", - "libraries": [ - { - "whl": "dbfs:/wheels/abfssintest-0.0.3-py3-none-any.whl" - } - ], - "timeout_seconds": 0 - } - ], - "format": "MULTI_TASK" - } + "name": "WheelJob", + "email_notifications": { + "no_alert_for_skipped_runs": false + }, + "max_concurrent_runs": 1, + "tasks": [ + { + "task_key": "WheelJob", + "python_wheel_task": { + "package_name": "abfssintest", + "entry_point": "runapp" + }, + "existing_cluster_id": "", + "libraries": [ + { + "whl": "dbfs:/FileStore/testcases/abfssintest-0.0.3-py3-none-any.whl" + } + ], + "timeout_seconds": 0 + } + ], + "format": "MULTI_TASK" } \ No newline at end of file diff --git a/tests/integration/jobdefs/pythonwheel-test-expectations.json b/tests/integration/jobdefs/pythonwheel-test-expectations.json index 922d6de..12ba684 100644 --- a/tests/integration/jobdefs/pythonwheel-test-expectations.json +++ b/tests/integration/jobdefs/pythonwheel-test-expectations.json @@ -1,5 +1,5 @@ [ "databricks://.azuredatabricks.net/jobs/", "databricks://.azuredatabricks.net/jobs//tasks/WheelJob", - "databricks://.azuredatabricks.net/jobs//tasks/WheelJob/processes/D18BCD0504F8604104FE4D0E7C821E13->50430216FAFCCDD3BFD497A1FA0C14D0" + "databricks://.azuredatabricks.net/jobs//tasks/WheelJob/processes/6438ED307BBA90F1285E1229E67E020B->5560AE0F6CE4403CC559ECF1821CCE47" ] \ No newline at end of file diff --git a/tests/integration/jobdefs/spark2-tests-def.json b/tests/integration/jobdefs/spark2-tests-def.json index b88b9be..e64037e 100644 --- a/tests/integration/jobdefs/spark2-tests-def.json +++ b/tests/integration/jobdefs/spark2-tests-def.json @@ -1,65 +1,63 @@ { - "settings": { - "name": "test-examples-spark-2", - "email_notifications": { - "no_alert_for_skipped_runs": false + "name": "test-examples-spark-2", + "email_notifications": { + "no_alert_for_skipped_runs": false + }, + "timeout_seconds": 0, + "max_concurrent_runs": 2, + "tasks": [ + { + "task_key": "spark2-abfss-in-abfss-out", + "notebook_task": { + "notebook_path": "/Shared/examples/abfss-in-abfss-out" + }, + "existing_cluster_id": "", + "timeout_seconds": 0, + "email_notifications": {}, + "description": "" }, - "timeout_seconds": 0, - "max_concurrent_runs": 2, - "tasks": [ - { - "task_key": "spark2-abfss-in-abfss-out", - "notebook_task": { - "notebook_path": "/Shared/examples/abfss-in-abfss-out" - }, - "existing_cluster_id": "0505-211804-c5x0jm8p", - "timeout_seconds": 0, - "email_notifications": {}, - "description": "" + { + "task_key": "spark2-abfss-oauth", + "depends_on": [ + { + "task_key": "spark2-abfss-in-abfss-out" + } + ], + "notebook_task": { + "notebook_path": "/Shared/examples/abfss-in-abfss-out-oauth" }, - { - "task_key": "spark2-abfss-oauth", - "depends_on": [ - { - "task_key": "spark2-abfss-in-abfss-out" - } - ], - "notebook_task": { - "notebook_path": "/Shared/examples/abfss-in-abfss-out-oauth" - }, - "existing_cluster_id": "0505-211804-c5x0jm8p", - "timeout_seconds": 0, - "email_notifications": {} + "existing_cluster_id": "", + "timeout_seconds": 0, + "email_notifications": {} + }, + { + "task_key": "spark2-mnt", + "depends_on": [ + { + "task_key": "spark2-abfss-oauth" + } + ], + "notebook_task": { + "notebook_path": "/Shared/examples/mnt-in-mnt-out" }, - { - "task_key": "spark2-mnt", - "depends_on": [ - { - "task_key": "spark2-abfss-oauth" - } - ], - "notebook_task": { - "notebook_path": "/Shared/examples/mnt-in-mnt-out" - }, - "existing_cluster_id": "0505-211804-c5x0jm8p", - "timeout_seconds": 0, - "email_notifications": {} + "existing_cluster_id": "", + "timeout_seconds": 0, + "email_notifications": {} + }, + { + "task_key": "spark2-Synapse-wasbs-synapse", + "depends_on": [ + { + "task_key": "spark2-mnt" + } + ], + "notebook_task": { + "notebook_path": "/Shared/examples/synapse-wasbs-in-synapse-out" }, - { - "task_key": "spark2-Synapse-wasbs-synapse", - "depends_on": [ - { - "task_key": "spark2-mnt" - } - ], - "notebook_task": { - "notebook_path": "/Shared/examples/synapse-wasbs-in-synapse-out" - }, - "existing_cluster_id": "0505-211804-c5x0jm8p", - "timeout_seconds": 0, - "email_notifications": {} - } - ], - "format": "MULTI_TASK" - } + "existing_cluster_id": "", + "timeout_seconds": 0, + "email_notifications": {} + } + ], + "format": "MULTI_TASK" } \ No newline at end of file diff --git a/tests/integration/jobdefs/spark2-tests-expectations.json b/tests/integration/jobdefs/spark2-tests-expectations.json index 2e6cefc..3b31266 100644 --- a/tests/integration/jobdefs/spark2-tests-expectations.json +++ b/tests/integration/jobdefs/spark2-tests-expectations.json @@ -1,13 +1,13 @@ [ "databricks://.azuredatabricks.net/jobs/", "databricks://.azuredatabricks.net/jobs//tasks/spark2-abfss-in-abfss-out", - "databricks://.azuredatabricks.net/jobs//tasks/spark2-abfss-in-abfss-out/processes/2796E46D0CCD18971A9C936C1EB97B1E->34BBA1402F1BAE560BFEA804B83FED62", + "databricks://.azuredatabricks.net/jobs//tasks/spark2-abfss-in-abfss-out/processes/58C1F24BA6C6FF7592F786C9FA8A3451->BA6B11F82FDCE37E849D25D545E6FB7A", "databricks://.azuredatabricks.net/notebooks/Shared/examples/abfss-in-abfss-out", "databricks://.azuredatabricks.net/jobs//tasks/spark2-abfss-oauth", - "databricks://.azuredatabricks.net/jobs//tasks/spark2-abfss-oauth/processes/56EE0B098A9A3D07DC11F4C6EA9BF71C->E6B1D99B74724B48DAB2BCB79142CB65", + "databricks://.azuredatabricks.net/jobs//tasks/spark2-abfss-oauth/processes/BD4A7A895E605BF6C4DE003D3F6B3F39->A3B52DA733083E4642E1C3DB6B093E84", "databricks://.azuredatabricks.net/notebooks/Shared/examples/abfss-in-abfss-out-oauth", "databricks://.azuredatabricks.net/jobs//tasks/spark2-mnt", - "databricks://.azuredatabricks.net/jobs//tasks/spark2-mnt/processes/EAEEF594372A61E0E1B545C0B430E966->ADFAB39F64A04DBD087DC73F8DF4EA47", + "databricks://.azuredatabricks.net/jobs//tasks/spark2-mnt/processes/336D6FD3010382DAB8351BFF026B2CBE->C60C4BAB82567905C64B99E2DCBCA711", "databricks://.azuredatabricks.net/notebooks/Shared/examples/mnt-in-mnt-out", "databricks://.azuredatabricks.net/jobs//tasks/spark2-Synapse-wasbs-synapse", "databricks://.azuredatabricks.net/jobs//tasks/spark2-Synapse-wasbs-synapse/processes/B596CF432EE21C0349CD0770BC839867->F1AD7C08349CD0A30B47392F787D6364", diff --git a/tests/integration/jobdefs/spark3-tests-def.json b/tests/integration/jobdefs/spark3-tests-def.json index e58b1c1..4a8f434 100644 --- a/tests/integration/jobdefs/spark3-tests-def.json +++ b/tests/integration/jobdefs/spark3-tests-def.json @@ -1,205 +1,203 @@ { - "settings": { - "name": "test-examples-spark-3", - "email_notifications": { - "no_alert_for_skipped_runs": false + "name": "test-examples-spark-3", + "email_notifications": { + "no_alert_for_skipped_runs": false + }, + "timeout_seconds": 0, + "max_concurrent_runs": 1, + "tasks": [ + { + "task_key": "abfss-in-abfss-out", + "notebook_task": { + "notebook_path": "/Shared/examples/abfss-in-abfss-out" + }, + "existing_cluster_id": "", + "timeout_seconds": 0, + "email_notifications": {}, + "description": "" }, - "timeout_seconds": 0, - "max_concurrent_runs": 1, - "tasks": [ - { - "task_key": "abfss-in-abfss-out", - "notebook_task": { - "notebook_path": "/Shared/examples/abfss-in-abfss-out" - }, - "existing_cluster_id": "", - "timeout_seconds": 0, - "email_notifications": {}, - "description": "" + { + "task_key": "abfss-oauth", + "depends_on": [ + { + "task_key": "abfss-in-abfss-out" + } + ], + "notebook_task": { + "notebook_path": "/Shared/examples/abfss-in-abfss-out-oauth" }, - { - "task_key": "abfss-oauth", - "depends_on": [ - { - "task_key": "abfss-in-abfss-out" - } - ], - "notebook_task": { - "notebook_path": "/Shared/examples/abfss-in-abfss-out-oauth" - }, - "existing_cluster_id": "", - "timeout_seconds": 0, - "email_notifications": {} + "existing_cluster_id": "", + "timeout_seconds": 0, + "email_notifications": {} + }, + { + "task_key": "azuresql-in-out", + "depends_on": [ + { + "task_key": "ab-in-ab-out-root" + } + ], + "notebook_task": { + "notebook_path": "/Shared/examples/azuresql-in-azuresql-out" }, - { - "task_key": "azuresql-in-out", - "depends_on": [ - { - "task_key": "ab-in-ab-out-root" - } - ], - "notebook_task": { - "notebook_path": "/Shared/examples/azuresql-in-azuresql-out" - }, - "existing_cluster_id": "", - "timeout_seconds": 0, - "email_notifications": {} + "existing_cluster_id": "", + "timeout_seconds": 0, + "email_notifications": {} + }, + { + "task_key": "delta-abfss", + "depends_on": [ + { + "task_key": "azuresql-in-out" + } + ], + "notebook_task": { + "notebook_path": "/Shared/examples/delta-in-delta-out-abfss" }, - { - "task_key": "delta-abfss", - "depends_on": [ - { - "task_key": "azuresql-in-out" - } - ], - "notebook_task": { - "notebook_path": "/Shared/examples/delta-in-delta-out-abfss" - }, - "existing_cluster_id": "", - "timeout_seconds": 0, - "email_notifications": {} + "existing_cluster_id": "", + "timeout_seconds": 0, + "email_notifications": {} + }, + { + "task_key": "delta-fs", + "depends_on": [ + { + "task_key": "delta-abfss" + } + ], + "notebook_task": { + "notebook_path": "/Shared/examples/delta-in-delta-out-fs" }, - { - "task_key": "delta-fs", - "depends_on": [ - { - "task_key": "delta-abfss" - } - ], - "notebook_task": { - "notebook_path": "/Shared/examples/delta-in-delta-out-fs" - }, - "existing_cluster_id": "", - "timeout_seconds": 0, - "email_notifications": {} + "existing_cluster_id": "", + "timeout_seconds": 0, + "email_notifications": {} + }, + { + "task_key": "delta-mnt", + "depends_on": [ + { + "task_key": "delta-fs" + } + ], + "notebook_task": { + "notebook_path": "/Shared/examples/delta-in-delta-out-mnt" }, - { - "task_key": "delta-mnt", - "depends_on": [ - { - "task_key": "delta-fs" - } - ], - "notebook_task": { - "notebook_path": "/Shared/examples/delta-in-delta-out-mnt" - }, - "existing_cluster_id": "", - "timeout_seconds": 0, - "email_notifications": {} + "existing_cluster_id": "", + "timeout_seconds": 0, + "email_notifications": {} + }, + { + "task_key": "mnt", + "depends_on": [ + { + "task_key": "intermix-languages" + } + ], + "notebook_task": { + "notebook_path": "/Shared/examples/mnt-in-mnt-out" }, - { - "task_key": "mnt", - "depends_on": [ - { - "task_key": "intermix-languages" - } - ], - "notebook_task": { - "notebook_path": "/Shared/examples/mnt-in-mnt-out" - }, - "existing_cluster_id": "", - "timeout_seconds": 0, - "email_notifications": {} + "existing_cluster_id": "", + "timeout_seconds": 0, + "email_notifications": {} + }, + { + "task_key": "synapse-in-wasbs-out", + "depends_on": [ + { + "task_key": "nested-parent" + } + ], + "notebook_task": { + "notebook_path": "/Shared/examples/synapse-in-wasbs-out" }, - { - "task_key": "synapse-in-wasbs-out", - "depends_on": [ - { - "task_key": "nested-parent" - } - ], - "notebook_task": { - "notebook_path": "/Shared/examples/synapse-in-wasbs-out" - }, - "existing_cluster_id": "", - "timeout_seconds": 0, - "email_notifications": {} + "existing_cluster_id": "", + "timeout_seconds": 0, + "email_notifications": {} + }, + { + "task_key": "Syn-in-WB-in-Syn-Out", + "depends_on": [ + { + "task_key": "synapse-in-wasbs-out" + } + ], + "notebook_task": { + "notebook_path": "/Shared/examples/synapse-wasbs-in-synapse-out" }, - { - "task_key": "Syn-in-WB-in-Syn-Out", - "depends_on": [ - { - "task_key": "synapse-in-wasbs-out" - } - ], - "notebook_task": { - "notebook_path": "/Shared/examples/synapse-wasbs-in-synapse-out" - }, - "existing_cluster_id": "", - "timeout_seconds": 0, - "email_notifications": {} + "existing_cluster_id": "", + "timeout_seconds": 0, + "email_notifications": {} + }, + { + "task_key": "wasbs-in-wasbs-out", + "depends_on": [ + { + "task_key": "Syn-in-WB-in-Syn-Out" + } + ], + "notebook_task": { + "notebook_path": "/Shared/examples/wasbs-in-wasbs-out" }, - { - "task_key": "wasbs-in-wasbs-out", - "depends_on": [ - { - "task_key": "Syn-in-WB-in-Syn-Out" - } - ], - "notebook_task": { - "notebook_path": "/Shared/examples/wasbs-in-wasbs-out" - }, - "existing_cluster_id": "", - "timeout_seconds": 0, - "email_notifications": {} + "existing_cluster_id": "", + "timeout_seconds": 0, + "email_notifications": {} + }, + { + "task_key": "ab-in-ab-out-root", + "depends_on": [ + { + "task_key": "abfss-oauth" + } + ], + "notebook_task": { + "notebook_path": "/Shared/examples/abfss-in-abfss-out-root" }, - { - "task_key": "ab-in-ab-out-root", - "depends_on": [ - { - "task_key": "abfss-oauth" - } - ], - "notebook_task": { - "notebook_path": "/Shared/examples/abfss-in-abfss-out-root" - }, - "existing_cluster_id": "", - "timeout_seconds": 0, - "email_notifications": {} + "existing_cluster_id": "", + "timeout_seconds": 0, + "email_notifications": {} + }, + { + "task_key": "nested-parent", + "depends_on": [ + { + "task_key": "mnt" + } + ], + "notebook_task": { + "notebook_path": "/Shared/examples/nested-parent" }, - { - "task_key": "nested-parent", - "depends_on": [ - { - "task_key": "mnt" - } - ], - "notebook_task": { - "notebook_path": "/Shared/examples/nested-parent" - }, - "existing_cluster_id": "", - "timeout_seconds": 0, - "email_notifications": {} + "existing_cluster_id": "", + "timeout_seconds": 0, + "email_notifications": {} + }, + { + "task_key": "intermix-languages", + "depends_on": [ + { + "task_key": "delta-mnt" + } + ], + "notebook_task": { + "notebook_path": "/Shared/examples/intermix-languages" }, - { - "task_key": "intermix-languages", - "depends_on": [ - { - "task_key": "delta-mnt" - } - ], - "notebook_task": { - "notebook_path": "/Shared/examples/intermix-languages" - }, - "existing_cluster_id": "", - "timeout_seconds": 0, - "email_notifications": {} + "existing_cluster_id": "", + "timeout_seconds": 0, + "email_notifications": {} + }, + { + "task_key": "output-with-period", + "depends_on": [ + { + "task_key": "nested-parent" + } + ], + "notebook_task": { + "notebook_path": "/Shared/examples/name-with-periods" }, - { - "task_key": "output-with-period", - "depends_on": [ - { - "task_key": "nested-parent" - } - ], - "notebook_task": { - "notebook_path": "/Shared/examples/name-with-periods" - }, - "existing_cluster_id": "", - "timeout_seconds": 0, - "email_notifications": {} - } - ], - "format": "MULTI_TASK" - } + "existing_cluster_id": "", + "timeout_seconds": 0, + "email_notifications": {} + } + ], + "format": "MULTI_TASK" } \ No newline at end of file diff --git a/tests/integration/jobdefs/spark3-tests-expectations.json b/tests/integration/jobdefs/spark3-tests-expectations.json index 2dca462..277e053 100644 --- a/tests/integration/jobdefs/spark3-tests-expectations.json +++ b/tests/integration/jobdefs/spark3-tests-expectations.json @@ -2,31 +2,31 @@ "databricks://.azuredatabricks.net/jobs/", "databricks://.azuredatabricks.net/jobs//tasks/Syn-in-WB-in-Syn-Out", "databricks://.azuredatabricks.net/jobs//tasks/ab-in-ab-out-root", - "databricks://.azuredatabricks.net/jobs//tasks/ab-in-ab-out-root/processes/E74B887E65E059D38DAB51D31A5432D8->F6D2E2554E30B80D0E6901908AADEDBB", + "databricks://.azuredatabricks.net/jobs//tasks/ab-in-ab-out-root/processes/0AFD1EDAC25ECE8253F387E74F28629A->1BB1B95C33EDB21B2D3903B9A8103087", "databricks://.azuredatabricks.net/jobs//tasks/abfss-in-abfss-out", - "databricks://.azuredatabricks.net/jobs//tasks/abfss-in-abfss-out/processes/2796E46D0CCD18971A9C936C1EB97B1E->34BBA1402F1BAE560BFEA804B83FED62", + "databricks://.azuredatabricks.net/jobs//tasks/abfss-in-abfss-out/processes/58C1F24BA6C6FF7592F786C9FA8A3451->BA6B11F82FDCE37E849D25D545E6FB7A", "databricks://.azuredatabricks.net/jobs//tasks/abfss-oauth", - "databricks://.azuredatabricks.net/jobs//tasks/abfss-oauth/processes/56EE0B098A9A3D07DC11F4C6EA9BF71C->E6B1D99B74724B48DAB2BCB79142CB65", + "databricks://.azuredatabricks.net/jobs//tasks/abfss-oauth/processes/BD4A7A895E605BF6C4DE003D3F6B3F39->A3B52DA733083E4642E1C3DB6B093E84", "databricks://.azuredatabricks.net/jobs//tasks/azuresql-in-out", - "databricks://.azuredatabricks.net/jobs//tasks/azuresql-in-out/processes/03CC0799BCA86B4A823AD9B6C9A772A1->1A1EF10BC89D10CA52B3559833DAC1F3", + "databricks://.azuredatabricks.net/jobs//tasks/azuresql-in-out/processes/B95334DF8F53EB63EDBA24AF88CFC7AA->80FC7C28AF3F669752CE8F2DA1987526", "databricks://.azuredatabricks.net/jobs//tasks/delta-abfss", - "databricks://.azuredatabricks.net/jobs//tasks/delta-abfss/processes/EEDA606783A7DD68C6A6C60221608209->0533EFDC2210DD1546DACD3291D14EE9", + "databricks://.azuredatabricks.net/jobs//tasks/delta-abfss/processes/CE0291670068E208B1A9621C1721730D->FD3D635F915390056518ECC38AB07DCC", "databricks://.azuredatabricks.net/jobs//tasks/delta-fs", - "databricks://.azuredatabricks.net/jobs//tasks/delta-fs/processes/C8A21C9CC03564B883DD6A2E4174F9AE->FC6DC149F25D86CE472C77290596DD9F", + "databricks://.azuredatabricks.net/jobs//tasks/delta-fs/processes/F0F4F25C04BAFB0FBFF90BE92709E7E4->9557C9A65FE7A9A7A89B6D9061C55B5A", "databricks://.azuredatabricks.net/jobs//tasks/delta-mnt", - "databricks://.azuredatabricks.net/jobs//tasks/delta-mnt/processes/F33E9424B73DFD1C3B8D0259EB772F87->E443883D4B66E0DBEE76C5331401E533", + "databricks://.azuredatabricks.net/jobs//tasks/delta-mnt/processes/A191E946F919A0717BB4FF2A79221996->3718CE24F8FCB01C633CF37CED45B3FC", "databricks://.azuredatabricks.net/jobs//tasks/intermix-languages", - "databricks://.azuredatabricks.net/jobs//tasks/intermix-languages/processes/837D6375622EA0C277BB0275C5B2E4BE->A950ACA0CBDF8EABD0C758E01B8893B3", + "databricks://.azuredatabricks.net/jobs//tasks/intermix-languages/processes/7D3D5D44FDC1DC865806712E633C5E56->B3CF5624F08EEEDF819869D074FA7774", "databricks://.azuredatabricks.net/jobs//tasks/mnt", - "databricks://.azuredatabricks.net/jobs//tasks/mnt/processes/EAEEF594372A61E0E1B545C0B430E966->ADFAB39F64A04DBD087DC73F8DF4EA47", - "databricks://.azuredatabricks.net/jobs//tasks/output-with-period", - "databricks://.1.azuredatabricks.net/jobs//tasks/output-with-period/processes/4AF18D6C70DDBCA092FC53396B2C908F->F0460570010BB248E2256F0F932A82B8", + "databricks://.azuredatabricks.net/jobs//tasks/mnt/processes/336D6FD3010382DAB8351BFF026B2CBE->C60C4BAB82567905C64B99E2DCBCA711", "databricks://.azuredatabricks.net/jobs//tasks/nested-parent", - "databricks://.azuredatabricks.net/jobs//tasks/nested-parent/processes/1611F7AEE100534D05476B8D8D8096A2->8F13AE1A6297C7B53E82AD0862A258C5", + "databricks://.azuredatabricks.net/jobs//tasks/nested-parent/processes/8514E8FCB25E967BC6DA61D1A48E2CD4->7C40325C08313ADDF8F653CACEAAA8C1", + "databricks://.azuredatabricks.net/jobs//tasks/output-with-period", + "databricks://.azuredatabricks.net/jobs//tasks/output-with-period/processes/8530DB90732944CA2C3C02E4FEE633E2->054707838715BECB4629ECF6B398BF1A", "databricks://.azuredatabricks.net/jobs//tasks/synapse-in-wasbs-out", - "databricks://.azuredatabricks.net/jobs//tasks/synapse-in-wasbs-out/processes/F8D4C5D21F4175BBF75031FA6F5C3C81->367B7F5964BD2C529BD8BF705A32802A", + "databricks://.azuredatabricks.net/jobs//tasks/synapse-in-wasbs-out/processes/FC4F9610428CB3C9FBCB97DF6D2B939D->76AAA8ADC61434F8BFB7C92E6ABF8C85", "databricks://.azuredatabricks.net/jobs//tasks/wasbs-in-wasbs-out", - "databricks://.azuredatabricks.net/jobs//tasks/wasbs-in-wasbs-out/processes/A85A7E9B093C1A818F2C4276C5A9A871->C903E6160BE06DD452AFE1AAD278162C", + "databricks://.azuredatabricks.net/jobs//tasks/wasbs-in-wasbs-out/processes/34DA3FD40AC2F55C125A86039355D6ED->4A56EEA94A2A249B6FA359EC03F43FF7", "databricks://.azuredatabricks.net/notebooks/Shared/examples/abfss-in-abfss-out", "databricks://.azuredatabricks.net/notebooks/Shared/examples/abfss-in-abfss-out-oauth", "databricks://.azuredatabricks.net/notebooks/Shared/examples/abfss-in-abfss-out-root", diff --git a/tests/integration/jobdefs/wasbs-in-wasbs-out-with-param-def.json b/tests/integration/jobdefs/wasbs-in-wasbs-out-with-param-def.json deleted file mode 100644 index e3c32df..0000000 --- a/tests/integration/jobdefs/wasbs-in-wasbs-out-with-param-def.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "settings": { - "name": "wasbs-in-wasbs-out-with-param", - "email_notifications": { - "no_alert_for_skipped_runs": false - }, - "timeout_seconds": 0, - "max_concurrent_runs": 1, - "tasks": [ - { - "task_key": "wasbs-in-wasbs-out-with-param", - "notebook_task": { - "notebook_path": "/Shared/examples/wasbs-in-wasbs-out-with-param", - "base_parameters": { - "myval": "10" - } - }, - "existing_cluster_id": "0326-140927-mc4qzaj5", - "timeout_seconds": 0, - "email_notifications": {} - } - ], - "format": "MULTI_TASK" - } -} \ No newline at end of file diff --git a/tests/integration/jobdefs/wasbs-in-wasbs-out-with-param-expectations.json b/tests/integration/jobdefs/wasbs-in-wasbs-out-with-param-expectations.json deleted file mode 100644 index a850eb6..0000000 --- a/tests/integration/jobdefs/wasbs-in-wasbs-out-with-param-expectations.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - "databricks://.azuredatabricks.net/jobs//tasks/wasbs-in-wasbs-out-with-param", - "databricks://.azuredatabricks.net/notebooks/Shared/examples/wasbs-in-wasbs-out-with-param", - "databricks://.azuredatabricks.net/jobs//tasks/wasbs-in-wasbs-out-with-param/processes/9E51CA344228BD2C592091F34BCF81B8->D4051C5A34E3E2812E191B59E82CB1B1", - "databricks://.azuredatabricks.net/jobs//tasks/wasbs-in-wasbs-out-with-param/processes/D4051C5A34E3E2812E191B59E82CB1B1->8031DA4E838D99236E94A4CE72C951BC" -] \ No newline at end of file diff --git a/tests/integration/run-test.sh b/tests/integration/run-test.sh index e31bc8e..002f5cd 100644 --- a/tests/integration/run-test.sh +++ b/tests/integration/run-test.sh @@ -43,8 +43,8 @@ for fn in `ls ./tests/integration/jobdefs`; do continue fi - # For each file, get the settings.name - job_name=$(cat "$TESTS_DIRECTORY/$fn" | jq -r '.settings.name') + # For each file, get the .name + job_name=$(cat "$TESTS_DIRECTORY/$fn" | jq -r '.name') echo "Preparing to run JobDef:$fn JobName:$job_name JobId:${jobnametoid[$job_name]}" temp_job_id=${jobnametoid[$job_name]} # Get the expectation file diff --git a/tests/integration/spark-apps/jarjobs/abfssInAbfssOut/app/src/main/java/SparkApp/Basic/App.java b/tests/integration/spark-apps/jarjobs/abfssInAbfssOut/app/src/main/java/SparkApp/Basic/App.java index 87b4bec..68da198 100644 --- a/tests/integration/spark-apps/jarjobs/abfssInAbfssOut/app/src/main/java/SparkApp/Basic/App.java +++ b/tests/integration/spark-apps/jarjobs/abfssInAbfssOut/app/src/main/java/SparkApp/Basic/App.java @@ -32,7 +32,7 @@ public static void main(String[] args) { System.out.println(new App().getGreeting()); - String storageKey = dbutils.secrets().get("purview-to-adb-scope", "storage-service-key"); + String storageKey = dbutils.secrets().get("purview-to-adb-kv", "storage-service-key"); spark.conf().set("fs.azure.account.key."+storageServiceName+".dfs.core.windows.net", storageKey); diff --git a/tests/integration/spark-apps/notebooks/abfss-in-abfss-out-oauth.scala b/tests/integration/spark-apps/notebooks/abfss-in-abfss-out-oauth.scala index 05702ab..59d3b7c 100644 --- a/tests/integration/spark-apps/notebooks/abfss-in-abfss-out-oauth.scala +++ b/tests/integration/spark-apps/notebooks/abfss-in-abfss-out-oauth.scala @@ -11,9 +11,9 @@ val outputRootPath = "abfss://"+ouptutContainerName+"@"+storageServiceName+".dfs spark.conf.set("fs.azure.account.auth.type."+storageServiceName+".dfs.core.windows.net", "OAuth") spark.conf.set("fs.azure.account.oauth.provider.type."+storageServiceName+".dfs.core.windows.net", "org.apache.hadoop.fs.azurebfs.oauth2.ClientCredsTokenProvider") -spark.conf.set("fs.azure.account.oauth2.client.id."+storageServiceName+".dfs.core.windows.net", dbutils.secrets.get("purview-to-adb-scope", "project-spn-client-id")) -spark.conf.set("fs.azure.account.oauth2.client.secret."+storageServiceName+".dfs.core.windows.net", dbutils.secrets.get("purview-to-adb-scope", "project-spn-secret")) -spark.conf.set("fs.azure.account.oauth2.client.endpoint."+storageServiceName+".dfs.core.windows.net", "https://login.microsoftonline.com/"+dbutils.secrets.get("purview-to-adb-scope", "tenant-id")+"/oauth2/token") +spark.conf.set("fs.azure.account.oauth2.client.id."+storageServiceName+".dfs.core.windows.net", dbutils.secrets.get("purview-to-adb-kv", "clientIdKey")) +spark.conf.set("fs.azure.account.oauth2.client.secret."+storageServiceName+".dfs.core.windows.net", dbutils.secrets.get("purview-to-adb-kv", "clientSecretKey")) +spark.conf.set("fs.azure.account.oauth2.client.endpoint."+storageServiceName+".dfs.core.windows.net", "https://login.microsoftonline.com/"+dbutils.secrets.get("purview-to-adb-kv", "tenant-id")+"/oauth2/token") // COMMAND ---------- diff --git a/tests/integration/spark-apps/notebooks/abfss-in-abfss-out-root.scala b/tests/integration/spark-apps/notebooks/abfss-in-abfss-out-root.scala index 7ed5f0a..6177e48 100644 --- a/tests/integration/spark-apps/notebooks/abfss-in-abfss-out-root.scala +++ b/tests/integration/spark-apps/notebooks/abfss-in-abfss-out-root.scala @@ -9,7 +9,7 @@ val ouptutContainerName = "writetoroot" val abfssRootPath = "abfss://"+storageContainerName+"@"+storageServiceName+".dfs.core.windows.net" val outputAbfssRootPath = "abfss://"+ouptutContainerName+"@"+storageServiceName+".dfs.core.windows.net/root" -val storageKey = dbutils.secrets.get("purview-to-adb-scope", "storage-service-key") +val storageKey = dbutils.secrets.get("purview-to-adb-kv", "storage-service-key") spark.conf.set("fs.azure.account.key."+storageServiceName+".dfs.core.windows.net", storageKey) diff --git a/tests/integration/spark-apps/notebooks/abfss-in-abfss-out.scala b/tests/integration/spark-apps/notebooks/abfss-in-abfss-out.scala index 82e3738..04c49a2 100644 --- a/tests/integration/spark-apps/notebooks/abfss-in-abfss-out.scala +++ b/tests/integration/spark-apps/notebooks/abfss-in-abfss-out.scala @@ -9,7 +9,7 @@ val ouptutContainerName = "outputdata" val abfssRootPath = "abfss://"+storageContainerName+"@"+storageServiceName+".dfs.core.windows.net" val outputRootPath = "abfss://"+ouptutContainerName+"@"+storageServiceName+".dfs.core.windows.net" -val storageKey = dbutils.secrets.get("purview-to-adb-scope", "storage-service-key") +val storageKey = dbutils.secrets.get("purview-to-adb-kv", "storage-service-key") spark.conf.set("fs.azure.account.key."+storageServiceName+".dfs.core.windows.net", storageKey) diff --git a/tests/integration/spark-apps/notebooks/abfss-in-hive+notmgd+saveAsTable-out.scala b/tests/integration/spark-apps/notebooks/abfss-in-hive+notmgd+saveAsTable-out.scala index 648fc45..d7ccd8d 100644 --- a/tests/integration/spark-apps/notebooks/abfss-in-hive+notmgd+saveAsTable-out.scala +++ b/tests/integration/spark-apps/notebooks/abfss-in-hive+notmgd+saveAsTable-out.scala @@ -18,7 +18,7 @@ val ouptutContainerName = "outputdata" val abfssRootPath = "abfss://"+storageContainerName+"@"+storageServiceName+".dfs.core.windows.net" val outputRootPath = "abfss://"+ouptutContainerName+"@"+storageServiceName+".dfs.core.windows.net" -val storageKey = dbutils.secrets.get("purview-to-adb-scope", "storage-service-key") +val storageKey = dbutils.secrets.get("purview-to-adb-kv", "storage-service-key") spark.conf.set("fs.azure.account.key."+storageServiceName+".dfs.core.windows.net", storageKey) diff --git a/tests/integration/spark-apps/notebooks/abfss-in-hive+saveAsTable-out.scala b/tests/integration/spark-apps/notebooks/abfss-in-hive+saveAsTable-out.scala index c6d6565..2922297 100644 --- a/tests/integration/spark-apps/notebooks/abfss-in-hive+saveAsTable-out.scala +++ b/tests/integration/spark-apps/notebooks/abfss-in-hive+saveAsTable-out.scala @@ -22,7 +22,7 @@ val ouptutContainerName = "outputdata" val abfssRootPath = "abfss://"+storageContainerName+"@"+storageServiceName+".dfs.core.windows.net" val outputRootPath = "abfss://"+ouptutContainerName+"@"+storageServiceName+".dfs.core.windows.net" -val storageKey = dbutils.secrets.get("purview-to-adb-scope", "storage-service-key") +val storageKey = dbutils.secrets.get("purview-to-adb-kv", "storage-service-key") spark.conf.set("fs.azure.account.key."+storageServiceName+".dfs.core.windows.net", storageKey) diff --git a/tests/integration/spark-apps/notebooks/azuresql-in-azuresql-out.scala b/tests/integration/spark-apps/notebooks/azuresql-in-azuresql-out.scala index a412c0f..c6bf66f 100644 --- a/tests/integration/spark-apps/notebooks/azuresql-in-azuresql-out.scala +++ b/tests/integration/spark-apps/notebooks/azuresql-in-azuresql-out.scala @@ -10,12 +10,10 @@ import java.lang.{ClassNotFoundException} // COMMAND ---------- -val server_name = "jdbc:sqlserver://FILL-IN-CONNECTION-STRING" -val database_name = "purview-to-adb-sqldb" -val url = server_name + ";" + "database=" + database_name + ";" +val url = dbutils.secrets.get("purview-to-adb-kv", "azuresql-jdbc-conn-str") -val username = dbutils.secrets.get("purview-to-adb-scope", "azuresql-username") -val password = dbutils.secrets.get("purview-to-adb-scope", "azuresql-password") +val username = dbutils.secrets.get("purview-to-adb-kv", "azuresql-username") +val password = dbutils.secrets.get("purview-to-adb-kv", "azuresql-password") // COMMAND ---------- diff --git a/tests/integration/spark-apps/notebooks/call-via-adf-spark2.scala b/tests/integration/spark-apps/notebooks/call-via-adf-spark2.scala index f2e147c..58c4039 100644 --- a/tests/integration/spark-apps/notebooks/call-via-adf-spark2.scala +++ b/tests/integration/spark-apps/notebooks/call-via-adf-spark2.scala @@ -9,7 +9,7 @@ val ouptutContainerName = "outputdata" val abfssRootPath = "abfss://"+storageContainerName+"@"+storageServiceName+".dfs.core.windows.net" val outputRootPath = "abfss://"+ouptutContainerName+"@"+storageServiceName+".dfs.core.windows.net" -val storageKey = dbutils.secrets.get("purview-to-adb-scope", "storage-service-key") +val storageKey = dbutils.secrets.get("purview-to-adb-kv", "storage-service-key") spark.conf.set("fs.azure.account.key."+storageServiceName+".dfs.core.windows.net", storageKey) diff --git a/tests/integration/spark-apps/notebooks/call-via-adf-spark3.scala b/tests/integration/spark-apps/notebooks/call-via-adf-spark3.scala index f4be397..c283939 100644 --- a/tests/integration/spark-apps/notebooks/call-via-adf-spark3.scala +++ b/tests/integration/spark-apps/notebooks/call-via-adf-spark3.scala @@ -9,7 +9,7 @@ val ouptutContainerName = "outputdata" val abfssRootPath = "abfss://"+storageContainerName+"@"+storageServiceName+".dfs.core.windows.net" val outputRootPath = "abfss://"+ouptutContainerName+"@"+storageServiceName+".dfs.core.windows.net" -val storageKey = dbutils.secrets.get("purview-to-adb-scope", "storage-service-key") +val storageKey = dbutils.secrets.get("purview-to-adb-kv", "storage-service-key") spark.conf.set("fs.azure.account.key."+storageServiceName+".dfs.core.windows.net", storageKey) diff --git a/tests/integration/spark-apps/notebooks/delta-in-delta-merge.scala b/tests/integration/spark-apps/notebooks/delta-in-delta-merge.scala index 81d88ff..f2fc4ed 100644 --- a/tests/integration/spark-apps/notebooks/delta-in-delta-merge.scala +++ b/tests/integration/spark-apps/notebooks/delta-in-delta-merge.scala @@ -11,13 +11,13 @@ val storageServiceName = sys.env("STORAGE_SERVICE_NAME") val storageContainerName = "rawdata" val abfssRootPath = "abfss://"+storageContainerName+"@"+storageServiceName+".dfs.core.windows.net" -val storageKey = dbutils.secrets.get("purview-to-adb-scope", "example-sa-key") +val storageKey = dbutils.secrets.get("purview-to-adb-kv", "storage-service-key") spark.conf.set("fs.azure.account.auth.type."+storageServiceName+".dfs.core.windows.net", "OAuth") spark.conf.set("fs.azure.account.oauth.provider.type."+storageServiceName+".dfs.core.windows.net", "org.apache.hadoop.fs.azurebfs.oauth2.ClientCredsTokenProvider") -spark.conf.set("fs.azure.account.oauth2.client.id."+storageServiceName+".dfs.core.windows.net", dbutils.secrets.get("purview-to-adb-scope", "project-spn-client-id")) -spark.conf.set("fs.azure.account.oauth2.client.secret."+storageServiceName+".dfs.core.windows.net", dbutils.secrets.get("purview-to-adb-scope", "project-spn-secret")) -spark.conf.set("fs.azure.account.oauth2.client.endpoint."+storageServiceName+".dfs.core.windows.net", "https://login.microsoftonline.com/"+dbutils.secrets.get("purview-to-adb-scope", "tenant-id")+"/oauth2/token") +spark.conf.set("fs.azure.account.oauth2.client.id."+storageServiceName+".dfs.core.windows.net", dbutils.secrets.get("purview-to-adb-kv", "clientIdKey")) +spark.conf.set("fs.azure.account.oauth2.client.secret."+storageServiceName+".dfs.core.windows.net", dbutils.secrets.get("purview-to-adb-kv", "clientSecretKey")) +spark.conf.set("fs.azure.account.oauth2.client.endpoint."+storageServiceName+".dfs.core.windows.net", "https://login.microsoftonline.com/"+dbutils.secrets.get("purview-to-adb-kv", "tenant-id")+"/oauth2/token") // COMMAND ---------- diff --git a/tests/integration/spark-apps/notebooks/delta-in-delta-out-abfss.scala b/tests/integration/spark-apps/notebooks/delta-in-delta-out-abfss.scala index 03e31de..6989356 100644 --- a/tests/integration/spark-apps/notebooks/delta-in-delta-out-abfss.scala +++ b/tests/integration/spark-apps/notebooks/delta-in-delta-out-abfss.scala @@ -5,13 +5,13 @@ val ouptutContainerName = "outputdata" val abfssRootPath = "abfss://"+storageContainerName+"@"+storageServiceName+".dfs.core.windows.net" val outputRootPath = "abfss://"+ouptutContainerName+"@"+storageServiceName+".dfs.core.windows.net" -val storageKey = dbutils.secrets.get("purview-to-adb-scope", "example-sa-key") +val storageKey = dbutils.secrets.get("purview-to-adb-kv", "storage-service-key") spark.conf.set("fs.azure.account.auth.type."+storageServiceName+".dfs.core.windows.net", "OAuth") spark.conf.set("fs.azure.account.oauth.provider.type."+storageServiceName+".dfs.core.windows.net", "org.apache.hadoop.fs.azurebfs.oauth2.ClientCredsTokenProvider") -spark.conf.set("fs.azure.account.oauth2.client.id."+storageServiceName+".dfs.core.windows.net", dbutils.secrets.get("purview-to-adb-scope", "project-spn-client-id")) -spark.conf.set("fs.azure.account.oauth2.client.secret."+storageServiceName+".dfs.core.windows.net", dbutils.secrets.get("purview-to-adb-scope", "project-spn-secret")) -spark.conf.set("fs.azure.account.oauth2.client.endpoint."+storageServiceName+".dfs.core.windows.net", "https://login.microsoftonline.com/"+dbutils.secrets.get("purview-to-adb-scope", "tenant-id")+"/oauth2/token") +spark.conf.set("fs.azure.account.oauth2.client.id."+storageServiceName+".dfs.core.windows.net", dbutils.secrets.get("purview-to-adb-kv", "clientIdKey")) +spark.conf.set("fs.azure.account.oauth2.client.secret."+storageServiceName+".dfs.core.windows.net", dbutils.secrets.get("purview-to-adb-kv", "clientSecretKey")) +spark.conf.set("fs.azure.account.oauth2.client.endpoint."+storageServiceName+".dfs.core.windows.net", "https://login.microsoftonline.com/"+dbutils.secrets.get("purview-to-adb-kv", "tenant-id")+"/oauth2/token") // COMMAND ---------- diff --git a/tests/integration/spark-apps/notebooks/delta-in-delta-out-fs.scala b/tests/integration/spark-apps/notebooks/delta-in-delta-out-fs.scala index d247e4f..3d3131a 100644 --- a/tests/integration/spark-apps/notebooks/delta-in-delta-out-fs.scala +++ b/tests/integration/spark-apps/notebooks/delta-in-delta-out-fs.scala @@ -5,14 +5,14 @@ val ouptutContainerName = "outputdata" val abfssRootPath = "abfss://"+storageContainerName+"@"+storageServiceName+".dfs.core.windows.net" val outputRootPath = "abfss://"+ouptutContainerName+"@"+storageServiceName+".dfs.core.windows.net" -val storageKey = dbutils.secrets.get("purview-to-adb-scope", "example-sa-key") +val storageKey = dbutils.secrets.get("purview-to-adb-kv", "storage-service-key") //spark.conf.set("fs.azure.account.key."+storageServiceName+".dfs.core.windows.net", storageKey) spark.conf.set("fs.azure.account.auth.type."+storageServiceName+".dfs.core.windows.net", "OAuth") spark.conf.set("fs.azure.account.oauth.provider.type."+storageServiceName+".dfs.core.windows.net", "org.apache.hadoop.fs.azurebfs.oauth2.ClientCredsTokenProvider") -spark.conf.set("fs.azure.account.oauth2.client.id."+storageServiceName+".dfs.core.windows.net", dbutils.secrets.get("purview-to-adb-scope", "project-spn-client-id")) -spark.conf.set("fs.azure.account.oauth2.client.secret."+storageServiceName+".dfs.core.windows.net", dbutils.secrets.get("purview-to-adb-scope", "project-spn-secret")) -spark.conf.set("fs.azure.account.oauth2.client.endpoint."+storageServiceName+".dfs.core.windows.net", "https://login.microsoftonline.com/"+dbutils.secrets.get("purview-to-adb-scope", "tenant-id")+"/oauth2/token") +spark.conf.set("fs.azure.account.oauth2.client.id."+storageServiceName+".dfs.core.windows.net", dbutils.secrets.get("purview-to-adb-kv", "clientIdKey")) +spark.conf.set("fs.azure.account.oauth2.client.secret."+storageServiceName+".dfs.core.windows.net", dbutils.secrets.get("purview-to-adb-kv", "clientSecretKey")) +spark.conf.set("fs.azure.account.oauth2.client.endpoint."+storageServiceName+".dfs.core.windows.net", "https://login.microsoftonline.com/"+dbutils.secrets.get("purview-to-adb-kv", "tenant-id")+"/oauth2/token") // COMMAND ---------- diff --git a/tests/integration/spark-apps/notebooks/delta-in-delta-out-mnt.scala b/tests/integration/spark-apps/notebooks/delta-in-delta-out-mnt.scala index 487c2e0..1d2f8c2 100644 --- a/tests/integration/spark-apps/notebooks/delta-in-delta-out-mnt.scala +++ b/tests/integration/spark-apps/notebooks/delta-in-delta-out-mnt.scala @@ -5,14 +5,14 @@ val ouptutContainerName = "outputdata" val abfssRootPath = "abfss://"+storageContainerName+"@"+storageServiceName+".dfs.core.windows.net" val outputRootPath = "abfss://"+ouptutContainerName+"@"+storageServiceName+".dfs.core.windows.net" -val storageKey = dbutils.secrets.get("purview-to-adb-scope", "example-sa-key") +val storageKey = dbutils.secrets.get("purview-to-adb-kv", "storage-service-key") //spark.conf.set("fs.azure.account.key."+storageServiceName+".dfs.core.windows.net", storageKey) spark.conf.set("fs.azure.account.auth.type."+storageServiceName+".dfs.core.windows.net", "OAuth") spark.conf.set("fs.azure.account.oauth.provider.type."+storageServiceName+".dfs.core.windows.net", "org.apache.hadoop.fs.azurebfs.oauth2.ClientCredsTokenProvider") -spark.conf.set("fs.azure.account.oauth2.client.id."+storageServiceName+".dfs.core.windows.net", dbutils.secrets.get("purview-to-adb-scope", "project-spn-client-id")) -spark.conf.set("fs.azure.account.oauth2.client.secret."+storageServiceName+".dfs.core.windows.net", dbutils.secrets.get("purview-to-adb-scope", "project-spn-secret")) -spark.conf.set("fs.azure.account.oauth2.client.endpoint."+storageServiceName+".dfs.core.windows.net", "https://login.microsoftonline.com/"+dbutils.secrets.get("purview-to-adb-scope", "tenant-id")+"/oauth2/token") +spark.conf.set("fs.azure.account.oauth2.client.id."+storageServiceName+".dfs.core.windows.net", dbutils.secrets.get("purview-to-adb-kv", "clientIdKey")) +spark.conf.set("fs.azure.account.oauth2.client.secret."+storageServiceName+".dfs.core.windows.net", dbutils.secrets.get("purview-to-adb-kv", "clientSecretKey")) +spark.conf.set("fs.azure.account.oauth2.client.endpoint."+storageServiceName+".dfs.core.windows.net", "https://login.microsoftonline.com/"+dbutils.secrets.get("purview-to-adb-kv", "tenant-id")+"/oauth2/token") // COMMAND ---------- diff --git a/tests/integration/spark-apps/notebooks/hive+abfss-in-hive+abfss-out-insert.py b/tests/integration/spark-apps/notebooks/hive+abfss-in-hive+abfss-out-insert.py index d0ea40d..d95ced1 100644 --- a/tests/integration/spark-apps/notebooks/hive+abfss-in-hive+abfss-out-insert.py +++ b/tests/integration/spark-apps/notebooks/hive+abfss-in-hive+abfss-out-insert.py @@ -7,7 +7,7 @@ abfssRootPath = "abfss://"+storageContainerName+"@"+storageServiceName+".dfs.core.windows.net" outputRootPath = "abfss://"+ouptutContainerName+"@"+storageServiceName+".dfs.core.windows.net" -storageKey = dbutils.secrets.get("purview-to-adb-scope", "storage-service-key") +storageKey = dbutils.secrets.get("purview-to-adb-kv", "storage-service-key") spark.conf.set("fs.azure.account.key."+storageServiceName+".dfs.core.windows.net", storageKey) spark.conf.set('spark.query.rootPath',abfssRootPath) @@ -15,25 +15,27 @@ # COMMAND ---------- -# MAGIC %sql -# MAGIC CREATE TABLE IF NOT EXISTS default.hiveExampleA001 ( -# MAGIC tableId INT, -# MAGIC x INT -# MAGIC ) -# MAGIC LOCATION 'abfss://rawdata@.dfs.core.windows.net/testcase/twentyone/exampleInputA/' -# MAGIC ; -# MAGIC -# MAGIC CREATE TABLE IF NOT EXISTS default.hiveExampleOutput001( -# MAGIC tableId INT, -# MAGIC x INT -# MAGIC ) -# MAGIC LOCATION 'abfss://rawdata@.dfs.core.windows.net/testcase/twentyone/exampleOutput/' -# MAGIC ; +spark.sql(f""" +CREATE TABLE IF NOT EXISTS default.testSample ( +tableId INT, +x INT +) +LOCATION 'abfss://rawdata@{storageServiceName}.dfs.core.windows.net/testcase/twentyone/exampleInputA/' +; +""" +) # COMMAND ---------- -# %sql -# INSERT INTO default.hiveExampleA001 (tableId, x) VALUES(1,2) +spark.sql(f""" +CREATE TABLE IF NOT EXISTS default.hiveExampleOutput001 ( +tableId INT, +x INT +) +LOCATION 'abfss://rawdata@{storageServiceName}.dfs.core.windows.net/testcase/twentyone/exampleOutput/' +; +""" +) # COMMAND ---------- @@ -44,12 +46,4 @@ # COMMAND ---------- -spark.read.table("default.hiveExampleOutput001").inputFiles() - -# COMMAND ---------- - -dbutils.fs.ls("abfss://rawdata@.dfs.core.windows.net/testcase/twentyone/exampleInputA/") - -# COMMAND ---------- - diff --git a/tests/integration/spark-apps/notebooks/hive+mgd+not+default-in-hive+mgd+not+default-out-insert.py b/tests/integration/spark-apps/notebooks/hive+mgd+not+default-in-hive+mgd+not+default-out-insert.py index 3bd5be9..18a99c8 100644 --- a/tests/integration/spark-apps/notebooks/hive+mgd+not+default-in-hive+mgd+not+default-out-insert.py +++ b/tests/integration/spark-apps/notebooks/hive+mgd+not+default-in-hive+mgd+not+default-out-insert.py @@ -1,28 +1,24 @@ # Databricks notebook source -# %sql -# CREATE DATABASE IF NOT EXISTS notdefault; +# MAGIC %sql +# MAGIC CREATE DATABASE IF NOT EXISTS notdefault; # COMMAND ---------- -# %sql -# CREATE TABLE IF NOT EXISTS notdefault.hiveExampleA ( -# tableId INT, -# x INT -# ); - -# CREATE TABLE notdefault.hiveExampleOutput( -# tableId INT, -# x INT -# ) - -# COMMAND ---------- +# MAGIC %sql +# MAGIC CREATE TABLE IF NOT EXISTS notdefault.hiveExampleA ( +# MAGIC tableId INT, +# MAGIC x INT +# MAGIC ); -# %sql -# INSERT INTO notdefault.hiveExampleA (tableId, x) VALUES(1,2) +# MAGIC CREATE TABLE IF NOT EXISTS notdefault.hiveExampleOutput( +# MAGIC tableId INT, +# MAGIC x INT +# MAGIC ) # COMMAND ---------- -spark.sparkContext.setLogLevel("DEBUG") +# MAGIC %sql +# MAGIC INSERT INTO notdefault.hiveExampleA (tableId, x) VALUES(1,2) # COMMAND ---------- @@ -32,10 +28,3 @@ # MAGIC FROM notdefault.hiveExampleA # COMMAND ---------- - -# MAGIC %md -# MAGIC # Exploring the File Path - -# COMMAND ---------- - -# dbutils.fs.ls("/user/hive/warehouse/notdefault.db/hiveexamplea") diff --git a/tests/integration/spark-apps/notebooks/hive-in-hive-out-insert.py b/tests/integration/spark-apps/notebooks/hive-in-hive-out-insert.py index 914a4c0..e68d33b 100644 --- a/tests/integration/spark-apps/notebooks/hive-in-hive-out-insert.py +++ b/tests/integration/spark-apps/notebooks/hive-in-hive-out-insert.py @@ -1,19 +1,14 @@ -# Datricks notebook source -# %sql -# CREATE TABLE IF NOT EXISTS default.hiveExampleA000 ( -# tableId INT, -# x INT -# ); - -# CREATE TABLE default.hiveExampleOutput000( -# tableId INT, -# x INT -# ) - -# COMMAND ---------- - -# %sql -# INSERT INTO default.hiveExampleA000 (tableId, x) VALUES(1,2) +# Databricks notebook source +# MAGIC %sql +# MAGIC CREATE TABLE IF NOT EXISTS default.hiveExampleA000 ( +# MAGIC tableId INT, +# MAGIC x INT +# MAGIC ); +# MAGIC +# MAGIC CREATE TABLE IF NOT EXISTS default.hiveExampleOutput000( +# MAGIC tableId INT, +# MAGIC x INT +# MAGIC ) # COMMAND ---------- diff --git a/tests/integration/spark-apps/notebooks/intermix-languages.scala b/tests/integration/spark-apps/notebooks/intermix-languages.scala index dee2728..83d51cf 100644 --- a/tests/integration/spark-apps/notebooks/intermix-languages.scala +++ b/tests/integration/spark-apps/notebooks/intermix-languages.scala @@ -9,14 +9,15 @@ val ouptutContainerName = "outputdata" val abfssRootPath = "abfss://"+storageContainerName+"@"+storageServiceName+".dfs.core.windows.net" val outputRootPath = "abfss://"+ouptutContainerName+"@"+storageServiceName+".dfs.core.windows.net" -val storageKey = dbutils.secrets.get("purview-to-adb-scope", "storage-service-key") +val storageKey = dbutils.secrets.get("purview-to-adb-kv", "storage-service-key") spark.conf.set("fs.azure.account.key."+storageServiceName+".dfs.core.windows.net", storageKey) // COMMAND ---------- // MAGIC %python -// MAGIC storageServiceName = sys.env("STORAGE_SERVICE_NAME") +// MAGIC import os +// MAGIC storageServiceName = os.environ.get("STORAGE_SERVICE_NAME") // MAGIC storageContainerName = "rawdata" // MAGIC ouptutContainerName = "outputdata" // MAGIC abfssRootPath = "abfss://"+storageContainerName+"@"+storageServiceName+".dfs.core.windows.net" diff --git a/tests/integration/spark-apps/notebooks/mnt-in-mnt-out.scala b/tests/integration/spark-apps/notebooks/mnt-in-mnt-out.scala index 2485288..92ed5d8 100644 --- a/tests/integration/spark-apps/notebooks/mnt-in-mnt-out.scala +++ b/tests/integration/spark-apps/notebooks/mnt-in-mnt-out.scala @@ -7,7 +7,7 @@ val storageServiceName = sys.env("STORAGE_SERVICE_NAME") val storageContainerName = "rawdata" val abfssRootPath = "abfss://"+storageContainerName+"@"+storageServiceName+".dfs.core.windows.net" -val storageKey = dbutils.secrets.get("purview-to-adb-scope", "example-sa-key") +val storageKey = dbutils.secrets.get("purview-to-adb-kv", "storage-service-key") spark.conf.set("fs.azure.account.key."+storageServiceName+".dfs.core.windows.net", storageKey) diff --git a/tests/integration/spark-apps/notebooks/name-with-periods.scala b/tests/integration/spark-apps/notebooks/name-with-periods.scala index f26aced..151b3d0 100644 --- a/tests/integration/spark-apps/notebooks/name-with-periods.scala +++ b/tests/integration/spark-apps/notebooks/name-with-periods.scala @@ -9,7 +9,7 @@ val ouptutContainerName = "outputdata" val abfssRootPath = "abfss://"+storageContainerName+"@"+storageServiceName+".dfs.core.windows.net" val outputRootPath = "abfss://"+ouptutContainerName+"@"+storageServiceName+".dfs.core.windows.net" -val storageKey = dbutils.secrets.get("purview-to-adb-scope", "storage-service-key") +val storageKey = dbutils.secrets.get("purview-to-adb-kv", "storage-service-key") spark.conf.set("fs.azure.account.key."+storageServiceName+".dfs.core.windows.net", storageKey) diff --git a/tests/integration/spark-apps/notebooks/nested-child.scala b/tests/integration/spark-apps/notebooks/nested-child.scala index 5b98bbe..3cd2b5f 100644 --- a/tests/integration/spark-apps/notebooks/nested-child.scala +++ b/tests/integration/spark-apps/notebooks/nested-child.scala @@ -9,7 +9,7 @@ val ouptutContainerName = "outputdata" val abfssRootPath = "abfss://"+storageContainerName+"@"+storageServiceName+".dfs.core.windows.net" val outputRootPath = "abfss://"+ouptutContainerName+"@"+storageServiceName+".dfs.core.windows.net" -val storageKey = dbutils.secrets.get("purview-to-adb-scope", "storage-service-key") +val storageKey = dbutils.secrets.get("purview-to-adb-kv", "storage-service-key") spark.conf.set("fs.azure.account.key."+storageServiceName+".dfs.core.windows.net", storageKey) diff --git a/tests/integration/spark-apps/notebooks/spark-sql-table-in-abfss-out.scala b/tests/integration/spark-apps/notebooks/spark-sql-table-in-abfss-out.scala index 74f2f8f..aa4fc63 100644 --- a/tests/integration/spark-apps/notebooks/spark-sql-table-in-abfss-out.scala +++ b/tests/integration/spark-apps/notebooks/spark-sql-table-in-abfss-out.scala @@ -46,7 +46,7 @@ val ouptutContainerName = "outputdata" val abfssRootPath = "abfss://"+storageContainerName+"@"+storageServiceName+".dfs.core.windows.net" val outputRootPath = "abfss://"+ouptutContainerName+"@"+storageServiceName+".dfs.core.windows.net" -val storageKey = dbutils.secrets.get("purview-to-adb-scope", "example-sa-key") +val storageKey = dbutils.secrets.get("purview-to-adb-kv", "storage-service-key") spark.conf.set("fs.azure.account.key."+storageServiceName+".dfs.core.windows.net", storageKey) diff --git a/tests/integration/spark-apps/notebooks/synapse-in-synapse-out.scala b/tests/integration/spark-apps/notebooks/synapse-in-synapse-out.scala index cfc10dc..c5e9892 100644 --- a/tests/integration/spark-apps/notebooks/synapse-in-synapse-out.scala +++ b/tests/integration/spark-apps/notebooks/synapse-in-synapse-out.scala @@ -1,33 +1,33 @@ // Databricks notebook source //Defining the service principal credentials for the Azure storage account -val tenantid = dbutils.secrets.get("purview-to-adb-scope", "tenant-id") +val tenantid = dbutils.secrets.get("purview-to-adb-kv", "tenant-id") val synapseStorageAccount = sys.env("SYNAPSE_STORAGE_SERVICE_NAME") spark.conf.set("fs.azure.account.auth.type", "OAuth") spark.conf.set("fs.azure.account.oauth.provider.type", "org.apache.hadoop.fs.azurebfs.oauth2.ClientCredsTokenProvider") -spark.conf.set("fs.azure.account.oauth2.client.id", dbutils.secrets.get("purview-to-adb-scope", "project-spn-client-id")) -spark.conf.set("fs.azure.account.oauth2.client.secret", dbutils.secrets.get("purview-to-adb-scope", "project-spn-secret")) +spark.conf.set("fs.azure.account.oauth2.client.id", dbutils.secrets.get("purview-to-adb-kv", "clientIdKey")) +spark.conf.set("fs.azure.account.oauth2.client.secret", dbutils.secrets.get("purview-to-adb-kv", "project-spn-secret")) spark.conf.set("fs.azure.account.oauth2.client.endpoint", "https://login.microsoftonline.com/" + tenantid + "/oauth2/token") //# Defining a separate set of service principal credentials for Azure Synapse Analytics (If not defined, the connector will use the Azure storage account credentials) -spark.conf.set("spark.databricks.sqldw.jdbc.service.principal.client.id", dbutils.secrets.get("purview-to-adb-scope", "project-spn-client-id")) -spark.conf.set("spark.databricks.sqldw.jdbc.service.principal.client.secret", dbutils.secrets.get("purview-to-adb-scope", "project-spn-secret")) -spark.conf.set("fs.azure.account.key."+synapseStorageAccount+".blob.core.windows.net", dbutils.secrets.get("purview-to-adb-scope", "synapse-storage-key")) +spark.conf.set("spark.databricks.sqldw.jdbc.service.principal.client.id", dbutils.secrets.get("purview-to-adb-kv", "clientIdKey")) +spark.conf.set("spark.databricks.sqldw.jdbc.service.principal.client.secret", dbutils.secrets.get("purview-to-adb-kv", "project-spn-secret")) +spark.conf.set("fs.azure.account.key."+synapseStorageAccount+".blob.core.windows.net", dbutils.secrets.get("purview-to-adb-kv", "synapse-storage-key")) // COMMAND ---------- //Azure Synapse related settings -val dwDatabase = "SQLPool1" +val dwDatabase = "sqlpool1" val dwServer = sys.env("SYNAPSE_SERVICE_NAME")+".sql.azuresynapse.net" -val dwUser = dbutils.secrets.get("purview-to-adb-scope", "synapse-query-username") -val dwPass = dbutils.secrets.get("purview-to-adb-scope", "synapse-query-password") +val dwUser = dbutils.secrets.get("purview-to-adb-kv", "synapse-query-username") +val dwPass = dbutils.secrets.get("purview-to-adb-kv", "synapse-query-password") val dwJdbcPort = "1433" val dwJdbcExtraOptions = "encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;" val sqlDwUrl = "jdbc:sqlserver://" + dwServer + ":" + dwJdbcPort + ";database=" + dwDatabase + ";user=" + dwUser+";password=" + dwPass + ";" + dwJdbcExtraOptions val blobStorage = synapseStorageAccount+".blob.core.windows.net" val blobContainer = "temp" -val blobAccessKey = dbutils.secrets.get("purview-to-adb-scope", "synapse-storage-key") +val blobAccessKey = dbutils.secrets.get("purview-to-adb-kv", "synapse-storage-key") val tempDir = "wasbs://" + blobContainer + "@" + blobStorage +"/tempfolder" // COMMAND ---------- diff --git a/tests/integration/spark-apps/notebooks/synapse-in-wasbs-out.scala b/tests/integration/spark-apps/notebooks/synapse-in-wasbs-out.scala index 90e0dc4..34ced24 100644 --- a/tests/integration/spark-apps/notebooks/synapse-in-wasbs-out.scala +++ b/tests/integration/spark-apps/notebooks/synapse-in-wasbs-out.scala @@ -1,40 +1,40 @@ // Databricks notebook source //Defining the service principal credentials for the Azure storage account -val tenantid = dbutils.secrets.get("purview-to-adb-scope", "tenant-id") +val tenantid = dbutils.secrets.get("purview-to-adb-kv", "tenant-id") val synapseStorageAccount = sys.env("SYNAPSE_STORAGE_SERVICE_NAME") spark.conf.set("fs.azure.account.auth.type", "OAuth") spark.conf.set("fs.azure.account.oauth.provider.type", "org.apache.hadoop.fs.azurebfs.oauth2.ClientCredsTokenProvider") -spark.conf.set("fs.azure.account.oauth2.client.id", dbutils.secrets.get("purview-to-adb-scope", "project-spn-client-id")) -spark.conf.set("fs.azure.account.oauth2.client.secret", dbutils.secrets.get("purview-to-adb-scope", "project-spn-secret")) +spark.conf.set("fs.azure.account.oauth2.client.id", dbutils.secrets.get("purview-to-adb-kv", "clientIdKey")) +spark.conf.set("fs.azure.account.oauth2.client.secret", dbutils.secrets.get("purview-to-adb-kv", "clientSecretKey")) spark.conf.set("fs.azure.account.oauth2.client.endpoint", "https://login.microsoftonline.com/" + tenantid + "/oauth2/token") //# Defining a separate set of service principal credentials for Azure Synapse Analytics (If not defined, the connector will use the Azure storage account credentials) -spark.conf.set("spark.databricks.sqldw.jdbc.service.principal.client.id", dbutils.secrets.get("purview-to-adb-scope", "project-spn-client-id")) -spark.conf.set("spark.databricks.sqldw.jdbc.service.principal.client.secret", dbutils.secrets.get("purview-to-adb-scope", "project-spn-secret")) -spark.conf.set("fs.azure.account.key."+synapseStorageAccount+".blob.core.windows.net", dbutils.secrets.get("purview-to-adb-scope", "synapse-storage-key")) +spark.conf.set("spark.databricks.sqldw.jdbc.service.principal.client.id", dbutils.secrets.get("purview-to-adb-kv", "clientIdKey")) +spark.conf.set("spark.databricks.sqldw.jdbc.service.principal.client.secret", dbutils.secrets.get("purview-to-adb-kv", "clientSecretKey")) +spark.conf.set("fs.azure.account.key."+synapseStorageAccount+".blob.core.windows.net", dbutils.secrets.get("purview-to-adb-kv", "synapse-storage-key")) // COMMAND ---------- //Azure Synapse related settings -val dwDatabase = "SQLPool1" +val dwDatabase = "sqlpool1" val dwServer = sys.env("SYNAPSE_SERVICE_NAME")+".sql.azuresynapse.net" -val dwUser = dbutils.secrets.get("purview-to-adb-scope", "synapse-query-username") -val dwPass = dbutils.secrets.get("purview-to-adb-scope", "synapse-query-password") +val dwUser = dbutils.secrets.get("purview-to-adb-kv", "synapse-query-username") +val dwPass = dbutils.secrets.get("purview-to-adb-kv", "synapse-query-password") val dwJdbcPort = "1433" val dwJdbcExtraOptions = "encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;" val sqlDwUrl = "jdbc:sqlserver://" + dwServer + ":" + dwJdbcPort + ";database=" + dwDatabase + ";user=" + dwUser+";password=" + dwPass + ";" + dwJdbcExtraOptions val blobStorage = synapseStorageAccount+".blob.core.windows.net" val blobContainer = "temp" -val blobAccessKey = dbutils.secrets.get("purview-to-adb-scope", "synapse-storage-key") +val blobAccessKey = dbutils.secrets.get("purview-to-adb-kv", "synapse-storage-key") val tempDir = "wasbs://" + blobContainer + "@" + blobStorage +"/tempfolder" val storageServiceName = sys.env("STORAGE_SERVICE_NAME") val storageContainerName = "outputdata" val wasbsRootPath = "wasbs://"+storageContainerName+"@"+storageServiceName+".blob.core.windows.net" -val storageKey = dbutils.secrets.get("purview-to-adb-scope", "example-sa-key") +val storageKey = dbutils.secrets.get("purview-to-adb-kv", "storage-service-key") spark.conf.set("fs.azure.account.key."+storageServiceName+".blob.core.windows.net", storageKey) diff --git a/tests/integration/spark-apps/notebooks/synapse-wasbs-in-synapse-out.scala b/tests/integration/spark-apps/notebooks/synapse-wasbs-in-synapse-out.scala index 4c7205e..643b5bf 100644 --- a/tests/integration/spark-apps/notebooks/synapse-wasbs-in-synapse-out.scala +++ b/tests/integration/spark-apps/notebooks/synapse-wasbs-in-synapse-out.scala @@ -2,42 +2,42 @@ import org.apache.spark.sql.types.{StructType, StructField, IntegerType, StringType} //Defining the service principal credentials for the Azure storage account -val tenantid = dbutils.secrets.get("purview-to-adb-scope", "tenant-id") +val tenantid = dbutils.secrets.get("purview-to-adb-kv", "tenant-id") val synapseStorageAccount = sys.env("SYNAPSE_STORAGE_SERVICE_NAME") spark.conf.set("fs.azure.account.auth.type", "OAuth") spark.conf.set("fs.azure.account.oauth.provider.type", "org.apache.hadoop.fs.azurebfs.oauth2.ClientCredsTokenProvider") -spark.conf.set("fs.azure.account.oauth2.client.id", dbutils.secrets.get("purview-to-adb-scope", "project-spn-client-id")) -spark.conf.set("fs.azure.account.oauth2.client.secret", dbutils.secrets.get("purview-to-adb-scope", "project-spn-secret")) +spark.conf.set("fs.azure.account.oauth2.client.id", dbutils.secrets.get("purview-to-adb-kv", "clientIdKey")) +spark.conf.set("fs.azure.account.oauth2.client.secret", dbutils.secrets.get("purview-to-adb-kv", "clientSecretKey")) spark.conf.set("fs.azure.account.oauth2.client.endpoint", "https://login.microsoftonline.com/" + tenantid + "/oauth2/token") //# Defining a separate set of service principal credentials for Azure Synapse Analytics (If not defined, the connector will use the Azure storage account credentials) -spark.conf.set("spark.databricks.sqldw.jdbc.service.principal.client.id", dbutils.secrets.get("purview-to-adb-scope", "project-spn-client-id")) -spark.conf.set("spark.databricks.sqldw.jdbc.service.principal.client.secret", dbutils.secrets.get("purview-to-adb-scope", "project-spn-secret")) -spark.conf.set("fs.azure.account.key."+synapseStorageAccount+".blob.core.windows.net", dbutils.secrets.get("purview-to-adb-scope", "synapse-storage-key")) +spark.conf.set("spark.databricks.sqldw.jdbc.service.principal.client.id", dbutils.secrets.get("purview-to-adb-kv", "clientIdKey")) +spark.conf.set("spark.databricks.sqldw.jdbc.service.principal.client.secret", dbutils.secrets.get("purview-to-adb-kv", "clientSecretKey")) +spark.conf.set("fs.azure.account.key."+synapseStorageAccount+".blob.core.windows.net", dbutils.secrets.get("purview-to-adb-kv", "synapse-storage-key")) val storageServiceName = sys.env("STORAGE_SERVICE_NAME") val storageContainerName = "rawdata" val wasbsRootPath = "wasbs://"+storageContainerName+"@"+storageServiceName+".blob.core.windows.net" -val storageKey = dbutils.secrets.get("purview-to-adb-scope", "example-sa-key") +val storageKey = dbutils.secrets.get("purview-to-adb-kv", "storage-service-key") spark.conf.set("fs.azure.account.key."+storageServiceName+".blob.core.windows.net", storageKey) // COMMAND ---------- //Azure Synapse related settings -val dwDatabase = "SQLPool1" +val dwDatabase = "sqlpool1" val dwServer = sys.env("SYNAPSE_SERVICE_NAME")+".sql.azuresynapse.net" -val dwUser = dbutils.secrets.get("purview-to-adb-scope", "synapse-query-username") -val dwPass = dbutils.secrets.get("purview-to-adb-scope", "synapse-query-password") +val dwUser = dbutils.secrets.get("purview-to-adb-kv", "synapse-query-username") +val dwPass = dbutils.secrets.get("purview-to-adb-kv", "synapse-query-password") val dwJdbcPort = "1433" val dwJdbcExtraOptions = "encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;" val sqlDwUrl = "jdbc:sqlserver://" + dwServer + ":" + dwJdbcPort + ";database=" + dwDatabase + ";user=" + dwUser+";password=" + dwPass + ";" + dwJdbcExtraOptions val blobStorage = synapseStorageAccount+".blob.core.windows.net" val blobContainer = "temp" -val blobAccessKey = dbutils.secrets.get("purview-to-adb-scope", "synapse-storage-key") +val blobAccessKey = dbutils.secrets.get("purview-to-adb-kv", "synapse-storage-key") val tempDir = "wasbs://" + blobContainer + "@" + blobStorage +"/tempfolder" // COMMAND ---------- diff --git a/tests/integration/spark-apps/notebooks/wasbs-in-wasbs-out-with-param.py b/tests/integration/spark-apps/notebooks/wasbs-in-wasbs-out-with-param.py deleted file mode 100644 index 8dcb9a7..0000000 --- a/tests/integration/spark-apps/notebooks/wasbs-in-wasbs-out-with-param.py +++ /dev/null @@ -1,39 +0,0 @@ -# Databricks notebook source -# MAGIC %md -# MAGIC # Sample Databricks Lineage Extraction witrh param - -# COMMAND ---------- - -myval = dbutils.widgets.text('mayval','') -print(myval) - -# COMMAND ---------- - -key = dbutils.secrets.get("purview-to-adb-scope", "storage-service-key") - -spark.conf.set( - "fs.azure.account.key..blob.core.windows.net", - key) - -# COMMAND ---------- - -retail = ( - spark.read.csv("wasbs://rawdata@.blob.core.windows.net/retail/", inferSchema=True, header=True) - .withColumnRenamed('Customer ID', 'CustomerId' ) - .drop("Invoice") -) -retail.write.mode("overwrite").parquet("wasbs://outputdata@.blob.core.windows.net/retail/wasbdemo") - -# COMMAND ---------- - -display(retail.take(2)) - -# COMMAND ---------- - -retail2 = spark.read.parquet("wasbs://outputdata@.blob.core.windows.net/retail/wasbdemo") -retail2 = retail2.withColumnRenamed('Quantity', 'QuantitySold').drop('Country') -retail2.write.mode("overwrite").parquet("wasbs://outputdata@.blob.core.windows.net/retail/wasbdemo_updated") - -# COMMAND ---------- - -# display(retail2.take(2)) diff --git a/tests/integration/spark-apps/notebooks/wasbs-in-wasbs-out.scala b/tests/integration/spark-apps/notebooks/wasbs-in-wasbs-out.scala index f369f99..4368da8 100644 --- a/tests/integration/spark-apps/notebooks/wasbs-in-wasbs-out.scala +++ b/tests/integration/spark-apps/notebooks/wasbs-in-wasbs-out.scala @@ -7,7 +7,7 @@ val storageServiceName = sys.env("STORAGE_SERVICE_NAME") val storageContainerName = "rawdata" val wasbsRootPath = "wasbs://"+storageContainerName+"@"+storageServiceName+".blob.core.windows.net" -val storageKey = dbutils.secrets.get("purview-to-adb-scope", "storage-service-key") +val storageKey = dbutils.secrets.get("purview-to-adb-kv", "storage-service-key") spark.conf.set("fs.azure.account.key."+storageServiceName+".blob.core.windows.net", storageKey) @@ -22,7 +22,7 @@ val exampleA = ( spark.read.format("csv") .schema(exampleASchema) .option("header", true) - .load(wasbsRootPath+"/examples/data/csv/exampleInputA/exampleInputA.csv") + .load(wasbsRootPath+"/testcase/wasinwasout/exampleInputA/") ) @@ -35,14 +35,14 @@ val exampleB = ( spark.read.format("csv") .schema(exampleBSchema) .option("header", true) - .load(wasbsRootPath+"/examples/data/csv/exampleInputB/exampleInputB.csv") + .load(wasbsRootPath+"/testcase/wasinwasout/exampleInputB/") ) // COMMAND ---------- val outputDf = exampleA.join(exampleB, exampleA("id") === exampleB("id"), "inner").drop(exampleB("id")) -outputDf.repartition(1).write.mode("overwrite").format("csv").save(wasbsRootPath+"/examples/data/csv/exampleOutputWASBS/") +outputDf.repartition(1).write.mode("overwrite").format("csv").save(wasbsRootPath+"/testcase/wasinwasout/exampleOutputWASBS/") // COMMAND ---------- diff --git a/tests/integration/spark-apps/pythonscript/pythonscript.json b/tests/integration/spark-apps/pythonscript/pythonscript.json index 3732532..a58a07b 100644 --- a/tests/integration/spark-apps/pythonscript/pythonscript.json +++ b/tests/integration/spark-apps/pythonscript/pythonscript.json @@ -4,7 +4,7 @@ "num_workers": 1, "spark_version": "9.1.x-scala2.12", "spark_conf": { - "spark.openlineage.url.param.code": "{{secrets/purview-to-adb-scope/Ol-Output-Api-Key}}", + "spark.openlineage.url.param.code": "{{secrets/purview-to-adb-kv/Ol-Output-Api-Key}}", "spark.openlineage.host": "https://YOURFUNCTION.azurewebsites.net", "spark.openlineage.namespace": "adb-123.1#ABC123", "spark.openlineage.version": "1" diff --git a/tests/integration/spark-apps/sparksubmit/sparksubmit.json b/tests/integration/spark-apps/sparksubmit/sparksubmit.json index 3fd25b5..d62c1b6 100644 --- a/tests/integration/spark-apps/sparksubmit/sparksubmit.json +++ b/tests/integration/spark-apps/sparksubmit/sparksubmit.json @@ -4,7 +4,7 @@ "num_workers": 1, "spark_version": "9.1.x-scala2.12", "spark_conf": { - "spark.openlineage.url.param.code": "{{secrets/purview-to-adb-scope/Ol-Output-Api-Key}}", + "spark.openlineage.url.param.code": "{{secrets/purview-to-adb-kv/Ol-Output-Api-Key}}", "spark.openlineage.host": "https://YOURFUNCTION.azurewebsites.net", "spark.openlineage.namespace": "YOURNAMESPACE#JOBNAME", "spark.openlineage.version": "1" diff --git a/tests/integration/spark-apps/wheeljobs/abfssInAbfssOut/abfssintest/main.py b/tests/integration/spark-apps/wheeljobs/abfssInAbfssOut/abfssintest/main.py index e36cf25..32486a4 100644 --- a/tests/integration/spark-apps/wheeljobs/abfssInAbfssOut/abfssintest/main.py +++ b/tests/integration/spark-apps/wheeljobs/abfssInAbfssOut/abfssintest/main.py @@ -17,7 +17,7 @@ def runapp(): abfssRootPath = "abfss://"+storageContainerName+"@"+storageServiceName+".dfs.core.windows.net" outputRootPath = "abfss://"+ouptutContainerName+"@"+storageServiceName+".dfs.core.windows.net" - storageKey = dbutils.secrets.get("purview-to-adb-scope", "storage-service-key") + storageKey = dbutils.secrets.get("purview-to-adb-kv", "storage-service-key") spark.conf.set("fs.azure.account.key."+storageServiceName+".dfs.core.windows.net", storageKey) diff --git a/tests/integration/spark-apps/wheeljobs/abfssInAbfssOut/db-job-def.json b/tests/integration/spark-apps/wheeljobs/abfssInAbfssOut/db-job-def.json deleted file mode 100644 index e89c7c1..0000000 --- a/tests/integration/spark-apps/wheeljobs/abfssInAbfssOut/db-job-def.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "settings": { - "existing_cluster_id": "TEST-CLUSTER-ID", - "libraries": [ - { - "whl": "dbfs:/wheels/abfssintest-0.0.3-py3-none-any.whl" - } - ], - "python_wheel_task": { - "packageName": "abfssintest", - "entryPoint": "runapp" - }, - "timeout_seconds": 0, - "email_notifications": {}, - "name": "WheelJob", - "max_concurrent_runs": 1 - } -} \ No newline at end of file From 73e656e09d7c251dd2ba38db09619991bcf8cd07 Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Tue, 3 Jan 2023 10:19:19 -0600 Subject: [PATCH 05/59] Resolving race condition based on app settings being deployed while ms deploy is going and event hub authorization rules deploying at the same time. Co-authored-by: hmoazam --- deployment/infra/newdeploymenttemp.json | 161 ++++++++++-------- tests/deployment/compare-app-settings.py | 5 +- .../test_arm_mapping_matches_json.py | 8 +- 3 files changed, 97 insertions(+), 77 deletions(-) diff --git a/deployment/infra/newdeploymenttemp.json b/deployment/infra/newdeploymenttemp.json index 074d98e..5aa070e 100644 --- a/deployment/infra/newdeploymenttemp.json +++ b/deployment/infra/newdeploymenttemp.json @@ -138,78 +138,7 @@ "properties": { "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", "httpsOnly": true, - "siteConfig": { - "appSettings": [ - { - "name": "AzureWebJobsStorage", - "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';EndpointSuffix=', environment().suffixes.storage, ';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value)]" - }, - { - "name": "FunctionStorage", - "value": "[concat('@Microsoft.KeyVault(VaultName=', variables('openlineageKeyVaultName'),';SecretName=',variables('functionStorageSecret'),')')]" - }, - { - "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", - "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';EndpointSuffix=', environment().suffixes.storage, ';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value)]" - }, - { - "name": "WEBSITE_CONTENTSHARE", - "value": "[toLower(variables('functionAppName'))]" - }, - { - "name": "FUNCTIONS_EXTENSION_VERSION", - "value": "~4" - }, - { - "name": "WEBSITE_NODE_DEFAULT_VERSION", - "value": "~10" - }, - { - "name": "APPINSIGHTS_INSTRUMENTATIONKEY", - "value": "[reference(resourceId('microsoft.insights/components', variables('applicationInsightsName')), '2020-02-02-preview').InstrumentationKey]" - }, - { - "name": "FUNCTIONS_WORKER_RUNTIME", - "value": "[variables('functionWorkerRuntime')]" - }, - { - "name": "EventHubName", - "value": "[variables('openlineageNameEventHubName')]" - }, - { - "name": "ListenToMessagesFromEventHub", - "value": "[concat('@Microsoft.KeyVault(VaultName=', variables('openlineageKeyVaultName'),';SecretName=',variables('EventHubConnectionSecretNameListen'),')')]" - }, - { - "name": "SendMessagesToEventHub", - "value": "[concat('@Microsoft.KeyVault(VaultName=', variables('openlineageKeyVaultName'),';SecretName=',variables('EventHubConnectionSecretNameSend'),')')]" - }, - { - "name": "EventHubConsumerGroup", - "value": "read" - }, - { - "name": "OlToPurviewMappings", - "value": "{\"olToPurviewMappings\":[{\"name\":\"wasbs\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"wasbs\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_blob_path\",\"purviewPrefix\":\"https\"},{\"name\":\"wasb\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"wasb\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_blob_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsBlobRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssBlobRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsBlob\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfs\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssBlob\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfss\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"synapseSqlNonDbo\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameSpcBodyParts[0]\",\"compare\":\"contains\",\"op2\":\"azuresynapse\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_synapse_dedicated_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"synapseSql\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameSpcBodyParts[0]\",\"compare\":\"contains\",\"op2\":\"azuresynapse\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/dbo/{nameGroups[0].parts[0]}\",\"purviewDataType\":\"azure_synapse_dedicated_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQLNonDbo\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameGroups\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0]}/{nameGroups[1]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQLNonDboNoDotsInNames\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQL\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/dbo/{nameGroups[0]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azurePostgresNonPublic\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"},{\"op1\":\"nameSpcConParts\",\"compare\":\">\",\"op2\":\"4\"},{\"op1\":\"nameSpcConParts[3]\",\"compare\":\"=\",\"op2\":\"azure\"}],\"qualifiedName\":\"postgresql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"azurePostgres\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameSpcConParts\",\"compare\":\">\",\"op2\":\"4\"},{\"op1\":\"nameSpcConParts[3]\",\"compare\":\"=\",\"op2\":\"azure\"}],\"qualifiedName\":\"postgresql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/public/{nameGroups[0]}\",\"purviewDataType\":\"azure_postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"postgresNonPublic\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"postgresql://servers/{nameSpcBodyParts[0]}:{nameSpcBodyParts[1]}/dbs/{nameSpcBodyParts[2]}/schemas/{nameGroups[0].parts[0]}/tables/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"postgres\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"}],\"qualifiedName\":\"postgresql://servers/{nameSpcBodyParts[0]}:{nameSpcBodyParts[1]}/dbs/{nameSpcBodyParts[2]}/schemas/public/tables/{nameGroups[0]}\",\"purviewDataType\":\"postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"hiveManagedTableNotDefault\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"dbfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"contains\",\"op2\":\"hive/warehouse\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"4\"}],\"qualifiedName\":\"{nameGroups[0].parts[3]}.{nameGroups[0].parts[5]}@{AdbWorkspaceUrl}\",\"purviewDataType\":\"hive_table\",\"purviewPrefix\":\"hive\"},{\"name\":\"hiveManagedTableDefault\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"dbfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"contains\",\"op2\":\"hive/warehouse\"}],\"qualifiedName\":\"default.{nameGroups[0].parts[3]}@{AdbWorkspaceUrl}\",\"purviewDataType\":\"hive_table\",\"purviewPrefix\":\"hive\"},{\"name\":\"azureMySql\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"mysql\"}],\"qualifiedName\":\"mysql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_mysql_table\",\"purviewPrefix\":\"mysql\"}]}" - }, - { - "name": "PurviewAccountName", - "value": "[variables('purviewAccountName')]" - }, - { - "name": "ClientID", - "value": "[concat('@Microsoft.KeyVault(VaultName=', variables('openlineageKeyVaultName'),';SecretName=',variables('clientidkey'),')')]" - }, - { - "name": "ClientSecret", - "value": "[concat('@Microsoft.KeyVault(VaultName=', variables('openlineageKeyVaultName'),';SecretName=',variables('clientsecretkey'),')')]" - }, - { - "name": "TenantId", - "value": "[subscription().tenantId]" - } - ] - } + "siteConfig": {} }, "resources": [ { @@ -218,12 +147,93 @@ "location": "[resourceGroup().location]", "apiVersion": "2020-06-01", "dependsOn": [ - "[concat('Microsoft.Web/sites/', variables('functionAppName'))]" + "[concat('Microsoft.Web/sites/', variables('functionAppName'))]", + "[concat('Microsoft.Web/sites/', variables('functionAppName'), '/config/web')]" ], "properties": { "packageUri": "http://aka.ms/APFunctions2-2" } - } + }, + { + "apiVersion": "2020-06-01", + "type": "config", + "name": "web", + "dependsOn": [ + "[concat('Microsoft.Web/sites/', variables('functionAppName'))]" + ], + "properties": { + "appSettings": [ + { + "name": "AzureWebJobsStorage", + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';EndpointSuffix=', environment().suffixes.storage, ';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value)]" + }, + { + "name": "FunctionStorage", + "value": "[concat('@Microsoft.KeyVault(VaultName=', variables('openlineageKeyVaultName'),';SecretName=',variables('functionStorageSecret'),')')]" + }, + { + "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';EndpointSuffix=', environment().suffixes.storage, ';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value)]" + }, + { + "name": "WEBSITE_CONTENTSHARE", + "value": "[toLower(variables('functionAppName'))]" + }, + { + "name": "FUNCTIONS_EXTENSION_VERSION", + "value": "~4" + }, + { + "name": "WEBSITE_NODE_DEFAULT_VERSION", + "value": "~10" + }, + { + "name": "APPINSIGHTS_INSTRUMENTATIONKEY", + "value": "[reference(resourceId('microsoft.insights/components', variables('applicationInsightsName')), '2020-02-02-preview').InstrumentationKey]" + }, + { + "name": "FUNCTIONS_WORKER_RUNTIME", + "value": "[variables('functionWorkerRuntime')]" + }, + { + "name": "EventHubName", + "value": "[variables('openlineageNameEventHubName')]" + }, + { + "name": "ListenToMessagesFromEventHub", + "value": "[concat('@Microsoft.KeyVault(VaultName=', variables('openlineageKeyVaultName'),';SecretName=',variables('EventHubConnectionSecretNameListen'),')')]" + }, + { + "name": "SendMessagesToEventHub", + "value": "[concat('@Microsoft.KeyVault(VaultName=', variables('openlineageKeyVaultName'),';SecretName=',variables('EventHubConnectionSecretNameSend'),')')]" + }, + { + "name": "EventHubConsumerGroup", + "value": "read" + }, + { + "name": "OlToPurviewMappings", + "value": "{\"olToPurviewMappings\":[{\"name\":\"wasbs\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"wasbs\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_blob_path\",\"purviewPrefix\":\"https\"},{\"name\":\"wasb\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"wasb\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_blob_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsBlobRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssBlobRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsBlob\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfs\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssBlob\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfss\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"synapseSqlNonDbo\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameSpcBodyParts[0]\",\"compare\":\"contains\",\"op2\":\"azuresynapse\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_synapse_dedicated_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"synapseSql\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameSpcBodyParts[0]\",\"compare\":\"contains\",\"op2\":\"azuresynapse\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/dbo/{nameGroups[0].parts[0]}\",\"purviewDataType\":\"azure_synapse_dedicated_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQLNonDbo\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameGroups\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0]}/{nameGroups[1]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQLNonDboNoDotsInNames\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQL\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/dbo/{nameGroups[0]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azurePostgresNonPublic\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"},{\"op1\":\"nameSpcConParts\",\"compare\":\">\",\"op2\":\"4\"},{\"op1\":\"nameSpcConParts[3]\",\"compare\":\"=\",\"op2\":\"azure\"}],\"qualifiedName\":\"postgresql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"azurePostgres\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameSpcConParts\",\"compare\":\">\",\"op2\":\"4\"},{\"op1\":\"nameSpcConParts[3]\",\"compare\":\"=\",\"op2\":\"azure\"}],\"qualifiedName\":\"postgresql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/public/{nameGroups[0]}\",\"purviewDataType\":\"azure_postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"postgresNonPublic\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"postgresql://servers/{nameSpcBodyParts[0]}:{nameSpcBodyParts[1]}/dbs/{nameSpcBodyParts[2]}/schemas/{nameGroups[0].parts[0]}/tables/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"postgres\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"}],\"qualifiedName\":\"postgresql://servers/{nameSpcBodyParts[0]}:{nameSpcBodyParts[1]}/dbs/{nameSpcBodyParts[2]}/schemas/public/tables/{nameGroups[0]}\",\"purviewDataType\":\"postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"hiveManagedTableNotDefault\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"dbfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"contains\",\"op2\":\"hive/warehouse\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"4\"}],\"qualifiedName\":\"{nameGroups[0].parts[3]}.{nameGroups[0].parts[5]}@{AdbWorkspaceUrl}\",\"purviewDataType\":\"hive_table\",\"purviewPrefix\":\"hive\"},{\"name\":\"hiveManagedTableDefault\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"dbfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"contains\",\"op2\":\"hive/warehouse\"}],\"qualifiedName\":\"default.{nameGroups[0].parts[3]}@{AdbWorkspaceUrl}\",\"purviewDataType\":\"hive_table\",\"purviewPrefix\":\"hive\"},{\"name\":\"azureMySql\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"mysql\"}],\"qualifiedName\":\"mysql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_mysql_table\",\"purviewPrefix\":\"mysql\"}]}" + }, + { + "name": "PurviewAccountName", + "value": "[variables('purviewAccountName')]" + }, + { + "name": "ClientID", + "value": "[concat('@Microsoft.KeyVault(VaultName=', variables('openlineageKeyVaultName'),';SecretName=',variables('clientidkey'),')')]" + }, + { + "name": "ClientSecret", + "value": "[concat('@Microsoft.KeyVault(VaultName=', variables('openlineageKeyVaultName'),';SecretName=',variables('clientsecretkey'),')')]" + }, + { + "name": "TenantId", + "value": "[subscription().tenantId]" + } + ] + } + } ] }, { @@ -447,7 +457,8 @@ "name": "[concat(variables('openlineageEventHubNameSpaceName'), '/ListenMessages')]", "tags": "[parameters('resourceTagValues')]", "dependsOn": [ - "[resourceId('Microsoft.EventHub/namespaces', variables('openlineageEventHubNameSpaceName'))]" + "[resourceId('Microsoft.EventHub/namespaces', variables('openlineageEventHubNameSpaceName'))]", + "[resourceId('Microsoft.EventHub/namespaces/AuthorizationRules', variables('openlineageEventHubNameSpaceName'), 'SendMessages')]" ], "properties": { "rights": [ diff --git a/tests/deployment/compare-app-settings.py b/tests/deployment/compare-app-settings.py index 88fc6e0..35c2f8f 100644 --- a/tests/deployment/compare-app-settings.py +++ b/tests/deployment/compare-app-settings.py @@ -56,7 +56,10 @@ app_settings_in_template = None for resource in arm_template["resources"]: if resource["type"] == "Microsoft.Web/sites": - app_settings_in_template = resource["properties"]["siteConfig"]["appSettings"] + for child_resource in resource.get("resources", []): + if child_resource.get("type") == "config" and child_resource.get("name") == "web": + app_settings_in_template = child_resource.get("properties", {}).get("appSettings", []) + break if app_settings_in_template is None: raise ValueError("Unable to extract the Microsoft.web/sites resources") diff --git a/tests/deployment/test_arm_mapping_matches_json.py b/tests/deployment/test_arm_mapping_matches_json.py index 274dd02..9d56c36 100644 --- a/tests/deployment/test_arm_mapping_matches_json.py +++ b/tests/deployment/test_arm_mapping_matches_json.py @@ -22,7 +22,13 @@ if resource["name"] != "[variables('functionAppName')]": continue - app_settings = resource.get("properties", {}).get("siteConfig", {}).get("appSettings", []) + web_config={} + for child_resource in resource.get("resources", []): + if child_resource.get("type") == "config" and child_resource.get("name") == "web": + web_config = child_resource + break + + app_settings = web_config.get("properties", {}).get("appSettings", []) for setting in app_settings: if setting["name"] != "OlToPurviewMappings": continue From 612f4c060d0add1d670e2b693069e411a6f13bf5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Feb 2023 04:50:19 +0000 Subject: [PATCH 06/59] Bump cryptography from 38.0.4 to 39.0.1 in /tests/environment Bumps [cryptography](https://github.com/pyca/cryptography) from 38.0.4 to 39.0.1. - [Release notes](https://github.com/pyca/cryptography/releases) - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/38.0.4...39.0.1) --- updated-dependencies: - dependency-name: cryptography dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- tests/environment/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/environment/requirements.txt b/tests/environment/requirements.txt index 30cf059..ecca015 100644 --- a/tests/environment/requirements.txt +++ b/tests/environment/requirements.txt @@ -6,7 +6,7 @@ certifi==2022.12.7 cffi==1.15.1 charset-normalizer==2.1.1 colorama==0.4.6 -cryptography==38.0.4 +cryptography==39.0.1 idna==3.4 importlib-metadata==5.1.0 isodate==0.6.1 From b74ad2bdc3c325b958fc0884706b56508e463439 Mon Sep 17 00:00:00 2001 From: Hanna Moazam Date: Mon, 6 Feb 2023 18:39:13 +0300 Subject: [PATCH 07/59] Added unit test and integration test for Azure MySQL, and updated LIMITATIONS.md to indicate MySQL support (#149) --- LIMITATIONS.md | 5 ++ .../Helpers/Parser/QnParserTests.cs | 4 + tests/environment/README.md | 5 ++ tests/environment/sources/mysql.bicep | 89 +++++++++++++++++++ .../integration/jobdefs/spark3-tests-def.json | 60 +++++++++++++ .../jobdefs/spark3-tests-expectations.json | 5 +- .../notebooks/mysql-in-mysql-out.py | 57 ++++++++++++ 7 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 tests/environment/sources/mysql.bicep create mode 100644 tests/integration/spark-apps/notebooks/mysql-in-mysql-out.py diff --git a/LIMITATIONS.md b/LIMITATIONS.md index b5def59..8a9d7f5 100644 --- a/LIMITATIONS.md +++ b/LIMITATIONS.md @@ -10,6 +10,7 @@ The solution accelerator supports a limited set of data sources to be ingested i * [Azure Synapse SQL Pools](#azure-synapse-sql-pools) * [Azure SQL DB](#azure-sql-db) * [Delta Lake](#delta-lake-file-format) +* [Azure MySQL](#azure-mysql) * [Other Data Sources and Limitations](#other-data-sources-and-limitations) ## Connecting to Assets in Purview @@ -71,6 +72,10 @@ Supports [Delta File Format](https://delta.io/). * Does not currently support the MERGE INTO statement due to differences between proprietary Databricks and Open Source Delta implementations. * Commands such as [Vacuum](https://docs.delta.io/latest/delta-utility.html#toc-entry-1) or [Optimize](https://docs.microsoft.com/en-us/azure/databricks/spark/latest/spark-sql/language-manual/delta-optimize) do not emit any lineage information and will not result in a Purview asset. +## Azure MySQL + +Supports Azure MySQL through [JDBC](https://learn.microsoft.com/en-us/azure/databricks/external-data/jdbc). + ## Other Data Sources and Limitations ### Lineage for Unsupported Data Sources diff --git a/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs b/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs index aa48fcc..472d711 100644 --- a/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs +++ b/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs @@ -104,6 +104,10 @@ public QnParserTests() [InlineData("sqlserver://purviewadbsynapsews.sql.azuresynapse.net:1433;database=SQLPool1;", "sales.region", "mssql://purviewadbsynapsews.sql.azuresynapse.net/SQLPool1/sales/region")] + // Azure MySQL + [InlineData("mysql://fikz4nmpfka4s.mysql.database.azure.com:3306/mydatabase", + "fruits", + "mysql://fikz4nmpfka4s.mysql.database.azure.com/mydatabase/fruits")] public void GetIdentifiers_OlSource_ReturnsPurviewIdentifier(string nameSpace, string name, string expectedResult) { diff --git a/tests/environment/README.md b/tests/environment/README.md index 7ae4daa..afd2436 100644 --- a/tests/environment/README.md +++ b/tests/environment/README.md @@ -28,6 +28,8 @@ Add Service Principal as user in Databricks. Enable mount points with `./tests/environment/dbfs/mounts.py` +Install mysql:mysql-connector-java:8.0.30 (version may vary based on cluster config) on the cluster. + Add Key Vault Secrets * `tenant-id` * `storage-service-key` @@ -37,6 +39,9 @@ Add Key Vault Secrets * `synapse-storage-key` * `synapse-query-username` * `synapse-query-password` + * `mysql-username` of the form `username@servername` + * `mysql-password` + * `mysql-hostname` the server name of the Azure MySQL resource * Update SQL Db and Synapse Server with AAD Admin * Add Service Principal for Databricks to connect to SQL sources diff --git a/tests/environment/sources/mysql.bicep b/tests/environment/sources/mysql.bicep new file mode 100644 index 0000000..ec32db2 --- /dev/null +++ b/tests/environment/sources/mysql.bicep @@ -0,0 +1,89 @@ +@description('Server Name for Azure database for MySQL') +param serverName string = uniqueString('mysql', resourceGroup().id) + +@description('Database administrator login name') +@minLength(1) +param administratorLogin string + +@description('Database administrator password') +@minLength(8) +@secure() +param administratorLoginPassword string + +@description('Azure database for MySQL compute capacity in vCores (2,4,8,16,32)') +param skuCapacity int = 2 + +@description('Azure database for MySQL sku name ') +param skuName string = 'B_Gen5_2' + +@description('Azure database for MySQL Sku Size ') +param SkuSizeMB int = 5120 + +@description('Azure database for MySQL pricing tier') +@allowed([ + 'Basic' + 'GeneralPurpose' + 'MemoryOptimized' +]) +param SkuTier string = 'Basic' + +@description('Azure database for MySQL sku family') +param skuFamily string = 'Gen5' + +@description('MySQL version') +@allowed([ + '5.6' + '5.7' + '8.0' +]) +param mysqlVersion string = '8.0' + +@description('Location for all resources.') +param location string = resourceGroup().location + +@description('MySQL Server backup retention days') +param backupRetentionDays int = 7 + +@description('Geo-Redundant Backup setting') +param geoRedundantBackup string = 'Disabled' + + +var firewallrules = [ + { + Name: 'rule1' + StartIpAddress: '0.0.0.0' + EndIpAddress: '255.255.255.255' + } +] + +resource mysqlDbServer 'Microsoft.DBforMySQL/servers@2017-12-01' = { + name: serverName + location: location + sku: { + name: skuName + tier: SkuTier + capacity: skuCapacity + size: '${SkuSizeMB}' //a string is expected here but a int for the storageProfile... + family: skuFamily + } + properties: { + createMode: 'Default' + version: mysqlVersion + administratorLogin: administratorLogin + administratorLoginPassword: administratorLoginPassword + storageProfile: { + storageMB: SkuSizeMB + backupRetentionDays: backupRetentionDays + geoRedundantBackup: geoRedundantBackup + } + } +} + +@batchSize(1) +resource firewallRules 'Microsoft.DBforMySQL/servers/firewallRules@2017-12-01' = [for rule in firewallrules: { + name: '${mysqlDbServer.name}/${rule.Name}' + properties: { + startIpAddress: rule.StartIpAddress + endIpAddress: rule.EndIpAddress + } +}] diff --git a/tests/integration/jobdefs/spark3-tests-def.json b/tests/integration/jobdefs/spark3-tests-def.json index 4a8f434..5c48436 100644 --- a/tests/integration/jobdefs/spark3-tests-def.json +++ b/tests/integration/jobdefs/spark3-tests-def.json @@ -197,6 +197,66 @@ "existing_cluster_id": "", "timeout_seconds": 0, "email_notifications": {} + }, + { + "task_key": "wasbs-in-kusto-out", + "depends_on": [ + { + "task_key": "output-with-period" + } + ], + "notebook_task": { + "notebook_path": "/Shared/examples/wasbs-in-kusto-out", + "source": "WORKSPACE" + }, + "existing_cluster_id": "0104-045638-iaecf5ne", + "timeout_seconds": 0, + "email_notifications": {} + }, + { + "task_key": "kusto-in-wasbs-out", + "depends_on": [ + { + "task_key": "wasbs-in-kusto-out" + } + ], + "notebook_task": { + "notebook_path": "/Shared/examples/kusto-in-wasbs-out", + "source": "WORKSPACE" + }, + "existing_cluster_id": "0104-045638-iaecf5ne", + "timeout_seconds": 0, + "email_notifications": {} + }, + { + "task_key": "postgres-in-postgres-out", + "depends_on": [ + { + "task_key": "kusto-in-wasbs-out" + } + ], + "notebook_task": { + "notebook_path": "/Shared/examples/postgres-in-postgres-out", + "source": "WORKSPACE" + }, + "existing_cluster_id": "0104-045638-iaecf5ne", + "timeout_seconds": 0, + "email_notifications": {} + }, + { + "task_key": "mysql-in-mysql-out", + "depends_on": [ + { + "task_key": "postgres-in-postgres-out" + } + ], + "notebook_task": { + "notebook_path": "/Shared/examples/mysql-in-mysql-out", + "source": "WORKSPACE" + }, + "existing_cluster_id": "0104-045638-iaecf5ne", + "timeout_seconds": 0, + "email_notifications": {} } ], "format": "MULTI_TASK" diff --git a/tests/integration/jobdefs/spark3-tests-expectations.json b/tests/integration/jobdefs/spark3-tests-expectations.json index 277e053..54d0be3 100644 --- a/tests/integration/jobdefs/spark3-tests-expectations.json +++ b/tests/integration/jobdefs/spark3-tests-expectations.json @@ -40,5 +40,8 @@ "databricks://.azuredatabricks.net/notebooks/Shared/examples/nested-parent", "databricks://.azuredatabricks.net/notebooks/Shared/examples/synapse-in-wasbs-out", "databricks://.azuredatabricks.net/notebooks/Shared/examples/synapse-wasbs-in-synapse-out", - "databricks://.azuredatabricks.net/notebooks/Shared/examples/wasbs-in-wasbs-out" + "databricks://.azuredatabricks.net/notebooks/Shared/examples/wasbs-in-wasbs-out", + "databricks://.azuredatabricks.net/notebooks/Shared/examples/mysql-in-mysql-out", + "databricks://.azuredatabricks.net/notebooks/Shared/examples/mysql-in-mysql-out/processes/1F6965315A6049825A37C4AD085BD605->A08160B244AF828E1FDB80AC8D14FA96", + "databricks://.azuredatabricks.net/jobs//tasks/mysql-in-mysql-out" ] \ No newline at end of file diff --git a/tests/integration/spark-apps/notebooks/mysql-in-mysql-out.py b/tests/integration/spark-apps/notebooks/mysql-in-mysql-out.py new file mode 100644 index 0000000..593f3a2 --- /dev/null +++ b/tests/integration/spark-apps/notebooks/mysql-in-mysql-out.py @@ -0,0 +1,57 @@ +# Databricks notebook source +# MAGIC %scala +# MAGIC Class.forName("com.mysql.cj.jdbc.Driver") + +# COMMAND ---------- + +host = dbutils.secrets.get("purview-to-adb-kv", "mysql-hostname") +user = dbutils.secrets.get("purview-to-adb-kv", "mysql-user") +password = dbutils.secrets.get("purview-to-adb-kv", "mysql-password") +database = "mydatabase" # hardcoded based on populate-data-mysql notebook. +table = "people" # hardcoded based on populate-data-mysql notebook. +port = "3306" # update if you use a non-default port +driver = "com.mysql.cj.jdbc.Driver" + +# COMMAND ---------- + +url = f"jdbc:mysql://{host}:{port}/{database}" + +df = (spark.read + .format("jdbc") + .option("driver", driver) + .option("url", url) + .option("dbtable", table) + .option("user", user) + .option("ssl", False) + .option("password", password) + .load() +) + +# COMMAND ---------- + +df.show() + +# COMMAND ---------- + +df=df.withColumn("age", df.age-100) + +# COMMAND ---------- + +df.show() + +# COMMAND ---------- + +df.write \ + .format("jdbc") \ + .option("driver", driver) \ + .option("url", url) \ + .option("dbtable", "fruits") \ + .option("user", user) \ + .option("ssl", False) \ + .mode("overwrite") \ + .option("password", password) \ + .save() + +# COMMAND ---------- + + From 6f4210890838a86d1eb19053e3b227d9746f0018 Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Sun, 5 Feb 2023 15:16:06 -0600 Subject: [PATCH 08/59] Documentation references OL 0.18 and DBR 11.3 --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- LIMITATIONS.md | 2 +- README.md | 4 ++-- TROUBLESHOOTING.md | 8 ++++---- deploy-base.md | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 7737b39..856d127 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -29,7 +29,7 @@ If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. Windows, Mac] - OpenLineage Version: [e.g. name of jar] - - Databricks Runtime Version: [e.g. 6.4, 9.1, 10.1] + - Databricks Runtime Version: [e.g. 9.1, 10.1, 11.3] - Cluster Type: [e.g. Job, Interactive] - Cluster Mode: [e.g. Standard, High Concurrency, Single] - Using Credential Passthrough: [e.g. Yes, No] diff --git a/LIMITATIONS.md b/LIMITATIONS.md index 8a9d7f5..506b7e5 100644 --- a/LIMITATIONS.md +++ b/LIMITATIONS.md @@ -120,7 +120,7 @@ The solution supports Spark 2 job cluster jobs. Databricks has removed Spark 2 f ### Spark 3.3+ Support -The solution supports Spark 3.0, 3.1, and 3.2 interactive and job clusters. We are working with the OpenLineage community to enable support of Spark 3.3 on Databricks Runtime 11.0 and higher. +The solution supports Spark 3.0, 3.1, 3.2, and 3.3 interactive and job clusters. The solution has been tested on the Databricks Runtime 11.3LTS version. ### Private Endpoints on Microsoft Purview diff --git a/README.md b/README.md index 6148d77..059e952 100644 --- a/README.md +++ b/README.md @@ -52,8 +52,8 @@ Gathering lineage data is performed in the following steps: * Azure Data Lake Gen 2 * Azure Blob Storage * Delta Lake -* Supports Spark 3.0, 3.1, and 3.2 (Interactive and Job clusters) / Spark 2.x (Job clusters) - * Databricks Runtimes between 6.4 and 10.4 are currently supported +* Supports Spark 3.0, 3.1, 3.2, and 3.3 (Interactive and Job clusters) / Spark 2.x (Job clusters) + * Databricks Runtimes between 9.1 and 11.3 LTS are currently supported * Can be configured per cluster or for all clusters as a global configuration * Once configured, **does not require any code changes to notebooks or jobs** * Can [add new source support through configuration](./docs/extending-source-support.md) diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index a71cc82..b7170a1 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -140,10 +140,10 @@ When reviewing the Driver logs, you see an error in the Log4j output that indica * Confirm that `spark.openlineage.version` is set correctly. |SA Release|OpenLineage Jar|spark.openlineage.version| - |----|----|----| - |1.0.0|0.8.2|1 - |1.1.0|0.8.2|1 - |2.0.0|0.11.0|v1 + |-----|----|----| + |1.0.x|0.8.2|1| + |1.1.x|0.8.2|1| + |2.x.x or newer|0.11.0 or newer|v1| ## PurviewOut Logs: Error Loading to Purview: 403 Forbidden diff --git a/deploy-base.md b/deploy-base.md index b1456ec..1fa5b5c 100644 --- a/deploy-base.md +++ b/deploy-base.md @@ -136,7 +136,7 @@ You will need the default API / Host key configured on your Function app. To ret ### Install OpenLineage on Your Databricks Cluster Follow the instructions below and refer to the [OpenLineage Databricks Install Instructions](https://github.com/OpenLineage/OpenLineage/tree/main/integration/spark/databricks#databricks-install-instructions) to enable OpenLineage in Databricks. -1. Download the [OpenLineage-Spark 0.13.0 jar](https://repo1.maven.org/maven2/io/openlineage/openlineage-spark/0.13.0/openlineage-spark-0.13.0.jar) from Maven Central +1. Download the [OpenLineage-Spark 0.18.0 jar](https://repo1.maven.org/maven2/io/openlineage/openlineage-spark/0.18.0/openlineage-spark-0.18.0.jar) from Maven Central 2. Create an init-script named `open-lineage-init-script.sh` ```text From e83d8acd3b8e61244e6bef246b7f01b7a7fbe36d Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Sun, 5 Feb 2023 15:23:52 -0600 Subject: [PATCH 09/59] No longer supporting Spark 2 --- .../{jobdefs => jobdefs-inactive}/spark2-tests-def.json | 0 .../{jobdefs => jobdefs-inactive}/spark2-tests-expectations.json | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/integration/{jobdefs => jobdefs-inactive}/spark2-tests-def.json (100%) rename tests/integration/{jobdefs => jobdefs-inactive}/spark2-tests-expectations.json (100%) diff --git a/tests/integration/jobdefs/spark2-tests-def.json b/tests/integration/jobdefs-inactive/spark2-tests-def.json similarity index 100% rename from tests/integration/jobdefs/spark2-tests-def.json rename to tests/integration/jobdefs-inactive/spark2-tests-def.json diff --git a/tests/integration/jobdefs/spark2-tests-expectations.json b/tests/integration/jobdefs-inactive/spark2-tests-expectations.json similarity index 100% rename from tests/integration/jobdefs/spark2-tests-expectations.json rename to tests/integration/jobdefs-inactive/spark2-tests-expectations.json From 7099eddc3e5b2d4f1c5d9c709ebca49c92a399cc Mon Sep 17 00:00:00 2001 From: Hanna Moazam Date: Mon, 6 Feb 2023 20:14:04 +0300 Subject: [PATCH 10/59] Updates - Postgres (#148) * Adding bicep file * Added unit and integration tests for Postgres. Updated LIMITATIONS.md to describe support and limitations --- LIMITATIONS.md | 10 ++ .../Helpers/Parser/QnParserTests.cs | 16 ++++ tests/environment/README.md | 4 + tests/environment/sources/postgres.bicep | 92 +++++++++++++++++++ .../jobdefs/spark3-tests-expectations.json | 5 +- .../notebooks/postgres-in-postgres-out.py | 50 ++++++++++ 6 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 tests/environment/sources/postgres.bicep create mode 100644 tests/integration/spark-apps/notebooks/postgres-in-postgres-out.py diff --git a/LIMITATIONS.md b/LIMITATIONS.md index 506b7e5..67e555f 100644 --- a/LIMITATIONS.md +++ b/LIMITATIONS.md @@ -11,6 +11,7 @@ The solution accelerator supports a limited set of data sources to be ingested i * [Azure SQL DB](#azure-sql-db) * [Delta Lake](#delta-lake-file-format) * [Azure MySQL](#azure-mysql) +* [PostgreSQL](#postgresql) * [Other Data Sources and Limitations](#other-data-sources-and-limitations) ## Connecting to Assets in Purview @@ -75,6 +76,15 @@ Supports [Delta File Format](https://delta.io/). ## Azure MySQL Supports Azure MySQL through [JDBC](https://learn.microsoft.com/en-us/azure/databricks/external-data/jdbc). +## PostgreSQL + +Supports both Azure PostgreSQL and on-prem/VM installations of PostgreSQL through [JDBC](https://learn.microsoft.com/en-us/azure/databricks/external-data/jdbc). + +* If you specify the `dbTable` value without the database schema (e.g. `dbo`), the connector assumes you are using the default `public` schema. + * For users and Service Principals with different default schemas, this may result in incorrect lineage. + * This can be corrected by specifying the database schema in the Spark job. +* Default configuration supports using multiple strings divided by dots to define a custom schema. For example ```myschema.mytable```. +* If you register and scan your postgres server as `localhost` in Microsoft Purview, but use the IP within the Databricks notebook, the assets will not be matched correctly. You need to use the IP when registering the Postgres server. ## Other Data Sources and Limitations diff --git a/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs b/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs index 472d711..269cf53 100644 --- a/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs +++ b/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs @@ -108,6 +108,22 @@ public QnParserTests() [InlineData("mysql://fikz4nmpfka4s.mysql.database.azure.com:3306/mydatabase", "fruits", "mysql://fikz4nmpfka4s.mysql.database.azure.com/mydatabase/fruits")] + // Azure Postgres Public + [InlineData("postgresql://gqhfuzgnrmpzw.postgres.database.azure.com:5432/postgres", + "people", + "postgresql://gqhfuzgnrmpzw.postgres.database.azure.com/postgres/public/people")] + // Azure Postgres Non Public + [InlineData("postgresql://gqhfuzgnrmpzw.postgres.database.azure.com:5432/postgres", + "myschema.people", + "postgresql://gqhfuzgnrmpzw.postgres.database.azure.com/postgres/myschema/people")] + // Postgres Public + [InlineData("postgresql://10.2.0.4:5432/postgres", + "table01", + "postgresql://servers/10.2.0.4:5432/dbs/postgres/schemas/public/tables/table01")] + // Postgres Non Public + [InlineData("postgresql://10.2.0.4:5432/postgres", + "myschema.table01", + "postgresql://servers/10.2.0.4:5432/dbs/postgres/schemas/myschema/tables/table01")] public void GetIdentifiers_OlSource_ReturnsPurviewIdentifier(string nameSpace, string name, string expectedResult) { diff --git a/tests/environment/README.md b/tests/environment/README.md index afd2436..573b64d 100644 --- a/tests/environment/README.md +++ b/tests/environment/README.md @@ -42,6 +42,10 @@ Add Key Vault Secrets * `mysql-username` of the form `username@servername` * `mysql-password` * `mysql-hostname` the server name of the Azure MySQL resource + * `postgres-admin-user` should be of the form `username@servername` + * `postgres-admin-password` + * `postgres-host` - the server name of the deployed postgres server + * Update SQL Db and Synapse Server with AAD Admin * Add Service Principal for Databricks to connect to SQL sources diff --git a/tests/environment/sources/postgres.bicep b/tests/environment/sources/postgres.bicep new file mode 100644 index 0000000..b58b7b2 --- /dev/null +++ b/tests/environment/sources/postgres.bicep @@ -0,0 +1,92 @@ +@description('Server Name for Azure database for PostgreSQL') +param serverName string = uniqueString('postgres', resourceGroup().id) + +@description('Database administrator login name') +@minLength(1) +param administratorLogin string + +@description('Database administrator password') +@minLength(8) +@secure() +param administratorLoginPassword string + +@description('Azure database for PostgreSQL compute capacity in vCores (2,4,8,16,32)') +param skuCapacity int = 2 + +@description('Azure database for PostgreSQL sku name') +param skuName string = 'B_Gen5_2' + +@description('Azure database for PostgreSQL Sku Size') +param skuSizeMB int = 5120 + +@description('Azure database for PostgreSQL pricing tier') +@allowed([ + 'Basic' + 'GeneralPurpose' + 'MemoryOptimized' +]) +param skuTier string = 'Basic' + +@description('Azure database for PostgreSQL sku family') +param skuFamily string = 'Gen5' + +@description('PostgreSQL version') +@allowed([ + '9.5' + '9.6' + '10' + '10.0' + '10.2' + '11' +]) +param postgresqlVersion string = '11' + +@description('Location for all resources.') +param location string = resourceGroup().location + +@description('PostgreSQL Server backup retention days') +param backupRetentionDays int = 7 + +@description('Geo-Redundant Backup setting') +param geoRedundantBackup string = 'Disabled' + +var firewallrules = [ + { + Name: 'rule1' + StartIpAddress: '0.0.0.0' + EndIpAddress: '255.255.255.255' + } +] + +resource server 'Microsoft.DBforPostgreSQL/servers@2017-12-01' = { + name: serverName + location: location + sku: { + name: skuName + tier: skuTier + capacity: skuCapacity + size: '${skuSizeMB}' + family: skuFamily + } + properties: { + createMode: 'Default' + version: postgresqlVersion + administratorLogin: administratorLogin + administratorLoginPassword: administratorLoginPassword + storageProfile: { + storageMB: skuSizeMB + backupRetentionDays: backupRetentionDays + geoRedundantBackup: geoRedundantBackup + } + } + +} + +@batchSize(1) +resource firewallRules 'Microsoft.DBforPostgreSQL/servers/firewallRules@2017-12-01' = [for rule in firewallrules: { + name: '${server.name}/${rule.Name}' + properties: { + startIpAddress: rule.StartIpAddress + endIpAddress: rule.EndIpAddress + } +}] diff --git a/tests/integration/jobdefs/spark3-tests-expectations.json b/tests/integration/jobdefs/spark3-tests-expectations.json index 54d0be3..e2064c8 100644 --- a/tests/integration/jobdefs/spark3-tests-expectations.json +++ b/tests/integration/jobdefs/spark3-tests-expectations.json @@ -43,5 +43,8 @@ "databricks://.azuredatabricks.net/notebooks/Shared/examples/wasbs-in-wasbs-out", "databricks://.azuredatabricks.net/notebooks/Shared/examples/mysql-in-mysql-out", "databricks://.azuredatabricks.net/notebooks/Shared/examples/mysql-in-mysql-out/processes/1F6965315A6049825A37C4AD085BD605->A08160B244AF828E1FDB80AC8D14FA96", - "databricks://.azuredatabricks.net/jobs//tasks/mysql-in-mysql-out" + "databricks://.azuredatabricks.net/jobs//tasks/mysql-in-mysql-out", + "databricks://.azuredatabricks.net/notebooks/Shared/examples/postgres-in-postgres-out", + "databricks://.azuredatabricks.net/notebooks/Shared/examples/postgres-in-postgres-out/processes/7E6CEF8EC093F119A11618169A8C4EAE->DB99105F739F05449E1ECD20A652DEE1", + "databricks://.azuredatabricks.net/jobs//tasks/postgres-in-postgres-out" ] \ No newline at end of file diff --git a/tests/integration/spark-apps/notebooks/postgres-in-postgres-out.py b/tests/integration/spark-apps/notebooks/postgres-in-postgres-out.py new file mode 100644 index 0000000..e02a81c --- /dev/null +++ b/tests/integration/spark-apps/notebooks/postgres-in-postgres-out.py @@ -0,0 +1,50 @@ +# Databricks notebook source +host = dbutils.secrets.get("purview-to-adb-kv", "postgres-host") +port = "5432" +dbname = "postgres" +user = dbutils.secrets.get("purview-to-adb-kv", "postgres-admin-user") +password = dbutils.secrets.get("purview-to-adb-kv", "postgres-admin-password") +table_in = "people" # hardcoded based on populate-data-postgres. +table_out = "fruits" +sslmode = "require" + +# COMMAND ---------- + +df = spark.read \ + .format("jdbc") \ + .option("url", f"jdbc:postgresql://{host}:{port}/{dbname}") \ + .option("dbtable", table_in) \ + .option("user", user) \ + .option("password", password) \ + .option("driver", "org.postgresql.Driver") \ + .option("ssl", False) \ + .load() + +# COMMAND ---------- + +df.show() + +# COMMAND ---------- + +df=df.withColumn("age", df.age-100) + +# COMMAND ---------- + +df.show() + +# COMMAND ---------- + +df.write \ + .format("jdbc") \ + .option("url", f"jdbc:postgresql://{host}:{port}/{dbname}") \ + .option("dbtable", table_out) \ + .option("user", user) \ + .option("password", password) \ + .option("driver", "org.postgresql.Driver") \ + .mode("overwrite") \ + .option("ssl", False) \ + .save() + +# COMMAND ---------- + + From 629660772a81f577faf388321c2fc3f280b75dcb Mon Sep 17 00:00:00 2001 From: Hanna Moazam Date: Mon, 6 Feb 2023 20:42:15 +0300 Subject: [PATCH 11/59] Feature/support kusto (#147) * Implemented support for kusto by updating mappings. * Implemented Kusto support and added unit and integration tests --- LIMITATIONS.md | 7 ++ deployment/infra/OlToPurviewMappings.json | 13 ++++ deployment/infra/newdeploymenttemp.json | 2 +- .../Helpers/Parser/QnParserTests.cs | 4 ++ tests/environment/README.md | 9 ++- tests/environment/sources/adx.bicep | 33 ++++++++++ .../integration/jobdefs/spark3-tests-def.json | 8 ++- .../jobdefs/spark3-tests-expectations.json | 8 ++- .../notebooks/kusto-in-wasbs-out.scala | 60 +++++++++++++++++ .../notebooks/populate-data-kusto.scala | 50 ++++++++++++++ .../notebooks/wasbs-in-kusto-out.scala | 66 +++++++++++++++++++ 11 files changed, 255 insertions(+), 5 deletions(-) create mode 100644 tests/environment/sources/adx.bicep create mode 100644 tests/integration/spark-apps/notebooks/kusto-in-wasbs-out.scala create mode 100644 tests/integration/spark-apps/notebooks/populate-data-kusto.scala create mode 100644 tests/integration/spark-apps/notebooks/wasbs-in-kusto-out.scala diff --git a/LIMITATIONS.md b/LIMITATIONS.md index 67e555f..0506c70 100644 --- a/LIMITATIONS.md +++ b/LIMITATIONS.md @@ -10,8 +10,12 @@ The solution accelerator supports a limited set of data sources to be ingested i * [Azure Synapse SQL Pools](#azure-synapse-sql-pools) * [Azure SQL DB](#azure-sql-db) * [Delta Lake](#delta-lake-file-format) +<<<<<<< HEAD * [Azure MySQL](#azure-mysql) * [PostgreSQL](#postgresql) +======= +* [Azure Data Explorer](#azure-data-explorer) +>>>>>>> e14c399 (Implemented Kusto support and added unit and integration tests) * [Other Data Sources and Limitations](#other-data-sources-and-limitations) ## Connecting to Assets in Purview @@ -85,6 +89,9 @@ Supports both Azure PostgreSQL and on-prem/VM installations of PostgreSQL throug * This can be corrected by specifying the database schema in the Spark job. * Default configuration supports using multiple strings divided by dots to define a custom schema. For example ```myschema.mytable```. * If you register and scan your postgres server as `localhost` in Microsoft Purview, but use the IP within the Databricks notebook, the assets will not be matched correctly. You need to use the IP when registering the Postgres server. +## Azure Data Explorer + +Supports Azure Data Explorer (aka Kusto) through the [Azure Data Explorer Connector for Apache Spark](https://learn.microsoft.com/en-us/azure/data-explorer/spark-connector) ## Other Data Sources and Limitations diff --git a/deployment/infra/OlToPurviewMappings.json b/deployment/infra/OlToPurviewMappings.json index ad30b9e..7c68f45 100644 --- a/deployment/infra/OlToPurviewMappings.json +++ b/deployment/infra/OlToPurviewMappings.json @@ -395,6 +395,19 @@ "qualifiedName": "mysql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/{nameGroups[0]}", "purviewDataType": "azure_mysql_table", "purviewPrefix": "mysql" + }, + { + "name": "kusto", + "parserConditions": [ + { + "op1": "prefix", + "compare": "=", + "op2": "azurekusto" + } + ], + "qualifiedName": "https://{nameSpcBodyParts[0]}/{nameSpcBodyParts[1]}/{nameGroups[0]}", + "purviewDataType": "azure_data_explorer_table", + "purviewPrefix": "https" } ] } \ No newline at end of file diff --git a/deployment/infra/newdeploymenttemp.json b/deployment/infra/newdeploymenttemp.json index 5aa070e..2674c85 100644 --- a/deployment/infra/newdeploymenttemp.json +++ b/deployment/infra/newdeploymenttemp.json @@ -213,7 +213,7 @@ }, { "name": "OlToPurviewMappings", - "value": "{\"olToPurviewMappings\":[{\"name\":\"wasbs\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"wasbs\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_blob_path\",\"purviewPrefix\":\"https\"},{\"name\":\"wasb\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"wasb\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_blob_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsBlobRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssBlobRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsBlob\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfs\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssBlob\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfss\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"synapseSqlNonDbo\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameSpcBodyParts[0]\",\"compare\":\"contains\",\"op2\":\"azuresynapse\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_synapse_dedicated_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"synapseSql\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameSpcBodyParts[0]\",\"compare\":\"contains\",\"op2\":\"azuresynapse\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/dbo/{nameGroups[0].parts[0]}\",\"purviewDataType\":\"azure_synapse_dedicated_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQLNonDbo\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameGroups\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0]}/{nameGroups[1]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQLNonDboNoDotsInNames\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQL\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/dbo/{nameGroups[0]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azurePostgresNonPublic\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"},{\"op1\":\"nameSpcConParts\",\"compare\":\">\",\"op2\":\"4\"},{\"op1\":\"nameSpcConParts[3]\",\"compare\":\"=\",\"op2\":\"azure\"}],\"qualifiedName\":\"postgresql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"azurePostgres\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameSpcConParts\",\"compare\":\">\",\"op2\":\"4\"},{\"op1\":\"nameSpcConParts[3]\",\"compare\":\"=\",\"op2\":\"azure\"}],\"qualifiedName\":\"postgresql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/public/{nameGroups[0]}\",\"purviewDataType\":\"azure_postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"postgresNonPublic\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"postgresql://servers/{nameSpcBodyParts[0]}:{nameSpcBodyParts[1]}/dbs/{nameSpcBodyParts[2]}/schemas/{nameGroups[0].parts[0]}/tables/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"postgres\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"}],\"qualifiedName\":\"postgresql://servers/{nameSpcBodyParts[0]}:{nameSpcBodyParts[1]}/dbs/{nameSpcBodyParts[2]}/schemas/public/tables/{nameGroups[0]}\",\"purviewDataType\":\"postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"hiveManagedTableNotDefault\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"dbfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"contains\",\"op2\":\"hive/warehouse\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"4\"}],\"qualifiedName\":\"{nameGroups[0].parts[3]}.{nameGroups[0].parts[5]}@{AdbWorkspaceUrl}\",\"purviewDataType\":\"hive_table\",\"purviewPrefix\":\"hive\"},{\"name\":\"hiveManagedTableDefault\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"dbfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"contains\",\"op2\":\"hive/warehouse\"}],\"qualifiedName\":\"default.{nameGroups[0].parts[3]}@{AdbWorkspaceUrl}\",\"purviewDataType\":\"hive_table\",\"purviewPrefix\":\"hive\"},{\"name\":\"azureMySql\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"mysql\"}],\"qualifiedName\":\"mysql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_mysql_table\",\"purviewPrefix\":\"mysql\"}]}" + "value": "{\"olToPurviewMappings\":[{\"name\":\"wasbs\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"wasbs\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_blob_path\",\"purviewPrefix\":\"https\"},{\"name\":\"wasb\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"wasb\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_blob_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsBlobRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssBlobRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsBlob\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfs\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssBlob\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfss\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"synapseSqlNonDbo\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameSpcBodyParts[0]\",\"compare\":\"contains\",\"op2\":\"azuresynapse\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_synapse_dedicated_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"synapseSql\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameSpcBodyParts[0]\",\"compare\":\"contains\",\"op2\":\"azuresynapse\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/dbo/{nameGroups[0].parts[0]}\",\"purviewDataType\":\"azure_synapse_dedicated_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQLNonDbo\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameGroups\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0]}/{nameGroups[1]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQLNonDboNoDotsInNames\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQL\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/dbo/{nameGroups[0]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azurePostgresNonPublic\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"},{\"op1\":\"nameSpcConParts\",\"compare\":\">\",\"op2\":\"4\"},{\"op1\":\"nameSpcConParts[3]\",\"compare\":\"=\",\"op2\":\"azure\"}],\"qualifiedName\":\"postgresql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"azurePostgres\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameSpcConParts\",\"compare\":\">\",\"op2\":\"4\"},{\"op1\":\"nameSpcConParts[3]\",\"compare\":\"=\",\"op2\":\"azure\"}],\"qualifiedName\":\"postgresql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/public/{nameGroups[0]}\",\"purviewDataType\":\"azure_postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"postgresNonPublic\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"postgresql://servers/{nameSpcBodyParts[0]}:{nameSpcBodyParts[1]}/dbs/{nameSpcBodyParts[2]}/schemas/{nameGroups[0].parts[0]}/tables/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"postgres\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"}],\"qualifiedName\":\"postgresql://servers/{nameSpcBodyParts[0]}:{nameSpcBodyParts[1]}/dbs/{nameSpcBodyParts[2]}/schemas/public/tables/{nameGroups[0]}\",\"purviewDataType\":\"postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"hiveManagedTableNotDefault\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"dbfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"contains\",\"op2\":\"hive/warehouse\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"4\"}],\"qualifiedName\":\"{nameGroups[0].parts[3]}.{nameGroups[0].parts[5]}@{AdbWorkspaceUrl}\",\"purviewDataType\":\"hive_table\",\"purviewPrefix\":\"hive\"},{\"name\":\"hiveManagedTableDefault\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"dbfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"contains\",\"op2\":\"hive/warehouse\"}],\"qualifiedName\":\"default.{nameGroups[0].parts[3]}@{AdbWorkspaceUrl}\",\"purviewDataType\":\"hive_table\",\"purviewPrefix\":\"hive\"},{\"name\":\"azureMySql\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"mysql\"}],\"qualifiedName\":\"mysql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_mysql_table\",\"purviewPrefix\":\"mysql\"},{\"name\":\"kusto\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"azurekusto\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[0]}/{nameSpcBodyParts[1]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_data_explorer_table\",\"purviewPrefix\":\"https\"}]}" }, { "name": "PurviewAccountName", diff --git a/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs b/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs index 269cf53..42f31d2 100644 --- a/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs +++ b/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs @@ -124,6 +124,10 @@ public QnParserTests() [InlineData("postgresql://10.2.0.4:5432/postgres", "myschema.table01", "postgresql://servers/10.2.0.4:5432/dbs/postgres/schemas/myschema/tables/table01")] + // Azure Data Explorer (Kusto) + [InlineData("azurekusto://qpll4l5hchczm.eastus2.kusto.windows.net/database01", + "table01", + "https://qpll4l5hchczm.eastus2.kusto.windows.net/database01/table01")] public void GetIdentifiers_OlSource_ReturnsPurviewIdentifier(string nameSpace, string name, string expectedResult) { diff --git a/tests/environment/README.md b/tests/environment/README.md index 573b64d..ee76c63 100644 --- a/tests/environment/README.md +++ b/tests/environment/README.md @@ -29,6 +29,7 @@ Add Service Principal as user in Databricks. Enable mount points with `./tests/environment/dbfs/mounts.py` Install mysql:mysql-connector-java:8.0.30 (version may vary based on cluster config) on the cluster. +Create/reuse a Service Principal for Azure Data Explorer Authentication. Create and save a secret locally. Add Key Vault Secrets * `tenant-id` @@ -45,9 +46,13 @@ Add Key Vault Secrets * `postgres-admin-user` should be of the form `username@servername` * `postgres-admin-password` * `postgres-host` - the server name of the deployed postgres server - + * `azurekusto-appid` + * `azurekusto-appsecret` + * `azurekusto-uri` + * Update SQL Db and Synapse Server with AAD Admin * Add Service Principal for Databricks to connect to SQL sources +* Assign the Service Principal admin role on the ADX cluster. [Guide](https://learn.microsoft.com/en-us/azure/data-explorer/provision-azure-ad-app#grant-the-service-principal-access-to-an-azure-data-explorer-database) Set the following system environments: @@ -55,6 +60,8 @@ Set the following system environments: * `STORAGE_SERVICE_NAME` * `SYNAPSE_STORAGE_SERVICE_NAME` +Install the version of the [kusto spark connector](https://github.com/Azure/azure-kusto-spark) that matches the cluster Scala and Spark versions from Maven Central. + Upload notebooks in `./tests/integration/spark-apps/notebooks/` to dbfs' `/Shared/examples/` * Manually for now. TODO: Automate this in Python diff --git a/tests/environment/sources/adx.bicep b/tests/environment/sources/adx.bicep new file mode 100644 index 0000000..02a174b --- /dev/null +++ b/tests/environment/sources/adx.bicep @@ -0,0 +1,33 @@ +@description('Cluster Name for Azure Data Explorer') +param clusterName string = uniqueString('adx', resourceGroup().id) + +@description('Database Name for Azure Data Explorer Cluster') +param databaseName string = 'database01' + +@description('Location for all resources.') +param location string = resourceGroup().location + +resource symbolicname 'Microsoft.Kusto/clusters@2022-11-11' = { + name: clusterName + location: location + sku: { + capacity: 1 + name: 'Dev(No SLA)_Standard_D11_v2' + tier: 'Basic' + } + identity: { + type: 'SystemAssigned' + } + properties: { + enableAutoStop: true + engineType: 'V3' + publicIPType: 'IPv4' + publicNetworkAccess: 'Enabled' + } + resource symbolicname 'databases@2022-11-11' = { + name: databaseName + location: location + kind: 'ReadWrite' + // For remaining properties, see clusters/databases objects + } +} diff --git a/tests/integration/jobdefs/spark3-tests-def.json b/tests/integration/jobdefs/spark3-tests-def.json index 5c48436..6c811bc 100644 --- a/tests/integration/jobdefs/spark3-tests-def.json +++ b/tests/integration/jobdefs/spark3-tests-def.json @@ -206,7 +206,11 @@ } ], "notebook_task": { +<<<<<<< HEAD "notebook_path": "/Shared/examples/wasbs-in-kusto-out", +======= + "notebook_path": "/Shared/examples/Test Kusto Write", +>>>>>>> e14c399 (Implemented Kusto support and added unit and integration tests) "source": "WORKSPACE" }, "existing_cluster_id": "0104-045638-iaecf5ne", @@ -239,7 +243,7 @@ "notebook_path": "/Shared/examples/postgres-in-postgres-out", "source": "WORKSPACE" }, - "existing_cluster_id": "0104-045638-iaecf5ne", + "existing_cluster_id": "", "timeout_seconds": 0, "email_notifications": {} }, @@ -254,7 +258,7 @@ "notebook_path": "/Shared/examples/mysql-in-mysql-out", "source": "WORKSPACE" }, - "existing_cluster_id": "0104-045638-iaecf5ne", + "existing_cluster_id": "", "timeout_seconds": 0, "email_notifications": {} } diff --git a/tests/integration/jobdefs/spark3-tests-expectations.json b/tests/integration/jobdefs/spark3-tests-expectations.json index e2064c8..e413897 100644 --- a/tests/integration/jobdefs/spark3-tests-expectations.json +++ b/tests/integration/jobdefs/spark3-tests-expectations.json @@ -46,5 +46,11 @@ "databricks://.azuredatabricks.net/jobs//tasks/mysql-in-mysql-out", "databricks://.azuredatabricks.net/notebooks/Shared/examples/postgres-in-postgres-out", "databricks://.azuredatabricks.net/notebooks/Shared/examples/postgres-in-postgres-out/processes/7E6CEF8EC093F119A11618169A8C4EAE->DB99105F739F05449E1ECD20A652DEE1", - "databricks://.azuredatabricks.net/jobs//tasks/postgres-in-postgres-out" + "databricks://.azuredatabricks.net/jobs//tasks/postgres-in-postgres-out", + "databricks://.azuredatabricks.net/notebooks/Shared/examples/wasbs-in-kusto-out", + "databricks://.azuredatabricks.net/jobs//tasks/wasbs-in-kusto-out", + "databricks://.azuredatabricks.net/jobs//tasks/wasbs-in-kusto-out/processes/D3E8F45D1D4150D809A56423DD2F2CFD->9D42C0F01CD2D2C2F014263C2D812602", + "databricks://.azuredatabricks.net/notebooks/Shared/examples/kusto-in-wasbs-out", + "databricks://.azuredatabricks.net/jobs//tasks/kusto-in-wasbs-out", + "databricks://.azuredatabricks.net/jobs//tasks/kusto-in-wasbs-out/processes/9D42C0F01CD2D2C2F014263C2D812602->55517236A8804C9548A4CB81814AEA6B" ] \ No newline at end of file diff --git a/tests/integration/spark-apps/notebooks/kusto-in-wasbs-out.scala b/tests/integration/spark-apps/notebooks/kusto-in-wasbs-out.scala new file mode 100644 index 0000000..5cb20d6 --- /dev/null +++ b/tests/integration/spark-apps/notebooks/kusto-in-wasbs-out.scala @@ -0,0 +1,60 @@ +// Databricks notebook source +spark.sparkContext.setLogLevel("ALL") + +// COMMAND ---------- + +import org.apache.commons.lang3.reflect.FieldUtils +import org.apache.commons.lang3.reflect.MethodUtils +import org.apache.spark.sql.execution.datasources.LogicalRelation +import com.microsoft.kusto.spark.datasink.KustoSinkOptions +import org.apache.spark.sql.{SaveMode, SparkSession} +import com.microsoft.kusto.spark.datasource.KustoSourceOptions +import org.apache.spark.SparkConf +import org.apache.spark.sql._ +import com.microsoft.azure.kusto.data.ClientRequestProperties +import com.microsoft.kusto.spark.sql.extension.SparkExtension._ +import com.microsoft.azure.kusto.data.ClientRequestProperties + +// COMMAND ---------- + +val appId = dbutils.secrets.get("purview-to-adb-kv", "azurekusto-appid") +val appKey = dbutils.secrets.get("purview-to-adb-kv", "azurekusto-appsecret") +val uri = dbutils.secrets.get("purview-to-adb-kv", "azurekusto-uri") +val authorityId = dbutils.secrets.get("purview-to-adb-kv", "tenant-id") +val cluster = uri.replaceAll(".kusto.windows.net", "").replaceAll("https://", "") +val database = "database01" // this is hardcoded - so if changed in the bicep template, also needs to be changed here. +val table = "table01" + +// COMMAND ---------- + +val conf: Map[String, String] = Map( + KustoSourceOptions.KUSTO_AAD_APP_ID -> appId, + KustoSourceOptions.KUSTO_AAD_APP_SECRET -> appKey, + KustoSourceOptions.KUSTO_AAD_AUTHORITY_ID -> authorityId + ) + +val df = spark.read.kusto(cluster, database, table, conf) + +// COMMAND ---------- + +val storageServiceName = sys.env("STORAGE_SERVICE_NAME") +val ouptutContainerName = "outputdata" + +val storageKey = dbutils.secrets.get("purview-to-adb-kv", "storage-service-key") + +spark.conf.set("fs.azure.account.key."+storageServiceName+".blob.core.windows.net", storageKey) + +// COMMAND ---------- + +val wasbsRootPath = "wasbs://"+ouptutContainerName+"@"+storageServiceName+".blob.core.windows.net" + +val file_location = wasbsRootPath+"/kusto/wasbs_out.csv" +val file_type = "csv" + +// COMMAND ---------- + +df.write.mode("overwrite").option("header","true").csv(file_location) + +// COMMAND ---------- + + diff --git a/tests/integration/spark-apps/notebooks/populate-data-kusto.scala b/tests/integration/spark-apps/notebooks/populate-data-kusto.scala new file mode 100644 index 0000000..ff04fc5 --- /dev/null +++ b/tests/integration/spark-apps/notebooks/populate-data-kusto.scala @@ -0,0 +1,50 @@ +// Databricks notebook source +spark.sparkContext.setLogLevel("ALL") + +// COMMAND ---------- + +import org.apache.commons.lang3.reflect.FieldUtils +import org.apache.commons.lang3.reflect.MethodUtils +import org.apache.spark.sql.execution.datasources.LogicalRelation +import com.microsoft.kusto.spark.datasink.KustoSinkOptions +import org.apache.spark.sql.{SaveMode, SparkSession} +import com.microsoft.kusto.spark.datasource.KustoSourceOptions +import org.apache.spark.SparkConf +import org.apache.spark.sql._ +import com.microsoft.azure.kusto.data.ClientRequestProperties +import com.microsoft.kusto.spark.sql.extension.SparkExtension._ +import com.microsoft.azure.kusto.data.ClientRequestProperties + +// COMMAND ---------- + +val appId = dbutils.secrets.get("purview-to-adb-kv", "azurekusto-appid") +val appKey = dbutils.secrets.get("purview-to-adb-kv", "azurekusto-appsecret") +val uri = dbutils.secrets.get("purview-to-adb-kv", "azurekusto-uri") +val authorityId = dbutils.secrets.get("purview-to-adb-kv", "tenant-id") +val cluster = uri.replaceAll(".kusto.windows.net", "").replaceAll("https://", "") +val database = "database01" // this is hardcoded - so if changed in the bicep template, also needs to be changed here. +val table = "table01" + +// COMMAND ---------- + +case class City(id: String, name: String, country: String) + +val df = Seq(new City("1", "Milwaukee", "USA"), new City("2", "Cairo", "Egypt"), new City("3", "Doha", "Qatar"), new City("4", "Kabul", "Afghanistan")).toDF + +// COMMAND ---------- + +df.write + .format("com.microsoft.kusto.spark.datasource") + .option(KustoSinkOptions.KUSTO_CLUSTER, cluster) + .option(KustoSinkOptions.KUSTO_DATABASE, database) + .option(KustoSinkOptions.KUSTO_TABLE, table) + .option(KustoSinkOptions.KUSTO_AAD_APP_ID, appId) + .option(KustoSinkOptions.KUSTO_AAD_APP_SECRET, appKey) + .option(KustoSinkOptions.KUSTO_AAD_AUTHORITY_ID, authorityId) + .option(KustoSinkOptions.KUSTO_TABLE_CREATE_OPTIONS, "CreateIfNotExist") + .mode(SaveMode.Append) + .save() + +// COMMAND ---------- + + diff --git a/tests/integration/spark-apps/notebooks/wasbs-in-kusto-out.scala b/tests/integration/spark-apps/notebooks/wasbs-in-kusto-out.scala new file mode 100644 index 0000000..ad6bf01 --- /dev/null +++ b/tests/integration/spark-apps/notebooks/wasbs-in-kusto-out.scala @@ -0,0 +1,66 @@ +// Databricks notebook source +spark.sparkContext.setLogLevel("DEBUG") + +// COMMAND ---------- + +import org.apache.commons.lang3.reflect.FieldUtils +import org.apache.commons.lang3.reflect.MethodUtils +import org.apache.spark.sql.execution.datasources.LogicalRelation +import com.microsoft.kusto.spark.datasink.KustoSinkOptions +import org.apache.spark.sql.{SaveMode, SparkSession} +import com.microsoft.kusto.spark.datasource.KustoSourceOptions +import org.apache.spark.SparkConf +import org.apache.spark.sql._ +import com.microsoft.azure.kusto.data.ClientRequestProperties +import com.microsoft.kusto.spark.sql.extension.SparkExtension._ +import com.microsoft.azure.kusto.data.ClientRequestProperties + +// COMMAND ---------- + +// MAGIC %md +// MAGIC ### Write + +// COMMAND ---------- + +val appId = dbutils.secrets.get("purview-to-adb-kv", "azurekusto-appid") +val appKey = dbutils.secrets.get("purview-to-adb-kv", "azurekusto-appsecret") +val uri = dbutils.secrets.get("purview-to-adb-kv", "azurekusto-uri") +val authorityId = dbutils.secrets.get("purview-to-adb-kv", "tenant-id") +val cluster = uri.replaceAll(".kusto.windows.net", "").replaceAll("https://", "") +val database = "database01" // this is hardcoded - so if changed in the bicep template, also needs to be changed here. +val table = "table01" + +// COMMAND ---------- + +val storageServiceName = sys.env("STORAGE_SERVICE_NAME") +val storageContainerName = "rawdata" +val wasbsRootPath = "wasbs://"+storageContainerName+"@"+storageServiceName+".blob.core.windows.net" + +val storageKey = dbutils.secrets.get("purview-to-adb-kv", "storage-service-key") + +spark.conf.set("fs.azure.account.key."+storageServiceName+".blob.core.windows.net", storageKey) + +val file_location = wasbsRootPath + "/testcase/one/exampleInputA/exampleInputA.csv" + +// COMMAND ---------- + +val df = spark.read.option("header","true").csv(file_location) + + +// COMMAND ---------- + +df.write + .format("com.microsoft.kusto.spark.datasource") + .option(KustoSinkOptions.KUSTO_CLUSTER, cluster) + .option(KustoSinkOptions.KUSTO_DATABASE, database) + .option(KustoSinkOptions.KUSTO_TABLE, table) + .option(KustoSinkOptions.KUSTO_AAD_APP_ID, appId) + .option(KustoSinkOptions.KUSTO_AAD_APP_SECRET, appKey) + .option(KustoSinkOptions.KUSTO_AAD_AUTHORITY_ID, authorityId) + .option(KustoSinkOptions.KUSTO_TABLE_CREATE_OPTIONS, "CreateIfNotExist") + .mode(SaveMode.Append) + .save() + +// COMMAND ---------- + + From f59a347f41633a39d2f1ba083d20fbd78ade9d90 Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Sat, 11 Feb 2023 20:41:04 -0600 Subject: [PATCH 12/59] Refactoring SelectReturnEntity to be more clear * checking for resource set types as its own method * checking for blob or data lake file path or filesystem types as its own method * refactored checking for adf relationships for blob/data lake file path types to its own method * Behavior Change for blob/data lake file path types that match: follow the insert into first position of valid entities rather than return a list with only the entity * Renamed ResourceSet_QualifiedNames_Match to QualifiedNames_Match_After_Normalizing and added the trim of the trailing slash for comaprisons * Using QualifiedNames_Match_After_Normalizing when comparing as the dfs vs blob comparison was preventing folder paths from matching if they're using wasbs but scanned for azure data lake gen2 (abfss) --- .../Helpers/PurviewCustomType.cs | 120 +++++++++++------- 1 file changed, 75 insertions(+), 45 deletions(-) diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs index 2a3fbca..a98a833 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs @@ -113,6 +113,28 @@ public bool IsSpark_Process(string typeName) return false; } + /// + /// Validate if the entity is a Blob or Data Lake entity type but not a resource set + /// + /// Type name + /// boolean + public bool IsBlobOrDataLakeFS_Entity(string typeName) + { + string typeNameLowercase = typeName.ToLower(); + return ((typeNameLowercase == "azure_blob_path") || (typeNameLowercase == "azure_datalake_gen2_path") || (typeNameLowercase == "azure_datalake_gen2_filesystem")); + } + + /// + /// Validate if the entity is a Blob or Data Lake resource type but not a file path or system type + /// + /// Type name + /// boolean + public bool IsBlobOrDataLakeResourceSet_Entity(string typeName) + { + string typeNameLowercase = typeName.ToLower(); + return ((typeNameLowercase == "azure_datalake_gen2_resource_set") || (typeNameLowercase == "azure_blob_resource_set")); + } + private void Init(string name, string typeName, string qualified_name, string data_type, string description, Int64 guid) { is_dummy_asset = false; @@ -385,11 +407,11 @@ private async Task> SelectReturnEntity(List> SelectReturnEntity(List 0) { - if (validEntities.Count > 0) - { - JObject folder = await _client.GetByGuid(entity.id); - if (folder.ContainsKey("entity")) - { - if (((JArray)folder!["entity"]!["relationshipAttributes"]!["inputToProcesses"]!).Count > 0) - { - foreach (JObject val in ((JArray)folder!["entity"]!["relationshipAttributes"]!["inputToProcesses"]!)) - { - if (val!["typeName"]!.ToString().ToLower().IndexOf("adf_") > -1) - { - validEntities = new List(); - validEntities.Add(entity); - return validEntities; - } - } - } - else - { - if (((JArray)folder!["entity"]!["relationshipAttributes"]!["outputFromProcesses"]!).Count > 0) - { - foreach (JObject val in ((JArray)folder!["entity"]!["relationshipAttributes"]!["outputFromProcesses"]!)) - { - if (val!["typeName"]!.ToString().ToLower().IndexOf("adf_") > -1) - { - validEntities = new List(); - validEntities.Add(entity); - return validEntities; - } - } - } - - } - } + JObject folder = await _client.GetByGuid(entity.id); + if (IsInputOrOutputOfAzureDataFactoryEntity(folder)){ + _logger.LogDebug($"Validating {this.to_compare_QualifiedName} vs {entity.qualifiedName} - Discovered entity is part of an ADF process and has been inserted first"); + validEntities.Insert(0,entity); + continue; } } - validEntities.Add(entity); } + validEntities.Add(entity); } } return validEntities; @@ -581,11 +576,46 @@ private string to_compare_QualifiedName // For resource sets, since Microsoft Purview cannot register the same storage account for // both ADLS G2 and Blob Storage, we need to match against either pattern (dfs.core.windows.net // or blob.core.windows.net) since we cannot be certain which one the end user has scanned. - private bool ResourceSet_QualifiedNames_Match(string entityOfInterestQualifiedName, string candidateQualifiedName){ - string _entityOfInterestFQN = entityOfInterestQualifiedName.ToLower().Trim().Replace(".dfs.core.windows.net","").Replace(".blob.core.windows.net",""); - string _candidateFQN = candidateQualifiedName.ToLower().Trim().Replace(".dfs.core.windows.net","").Replace(".blob.core.windows.net",""); + private bool QualifiedNames_Match_After_Normalizing(string entityOfInterestQualifiedName, string candidateQualifiedName) + { + string _entityOfInterestFQN = entityOfInterestQualifiedName.ToLower().Trim().Replace(".dfs.core.windows.net","").Replace(".blob.core.windows.net","").Trim('/'); + string _candidateFQN = candidateQualifiedName.ToLower().Trim().Replace(".dfs.core.windows.net","").Replace(".blob.core.windows.net","").Trim('/'); return _entityOfInterestFQN == _candidateFQN; } + + // Given an entity (presumably a blob or data lake folder), check to see if it has + // relationship attributes that indicate the entity is the input to or output of + // an azure data factory (adf_) process. + private bool IsInputOrOutputOfAzureDataFactoryEntity(JObject folder) + { + if (!folder.ContainsKey("entity")){ + return false; + } + + // TODO Refactor this to look across both inputs and outputs from one list + if (((JArray)folder!["entity"]!["relationshipAttributes"]!["inputToProcesses"]!).Count > 0) + { + foreach (JObject val in ((JArray)folder!["entity"]!["relationshipAttributes"]!["inputToProcesses"]!)) + { + if (val!["typeName"]!.ToString().ToLower().IndexOf("adf_") > -1) + { + return true; + } + } + } + else if (((JArray)folder!["entity"]!["relationshipAttributes"]!["outputFromProcesses"]!).Count > 0) + { + foreach (JObject val in ((JArray)folder!["entity"]!["relationshipAttributes"]!["outputFromProcesses"]!)) + { + if (val!["typeName"]!.ToString().ToLower().IndexOf("adf_") > -1) + { + return true; + } + } + } + // Fall through - If we didn't have an adf_ relationship + return false; + } } /// From dbccd6af2f28a162a4f184f52fc4cc2a62e37da8 Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Sat, 11 Feb 2023 21:05:16 -0600 Subject: [PATCH 13/59] Refactoring SelectReturnEntity to reflect we only accept entities with matching fully qualified names By pulling out and putting first the QualifiedNames_Match_After_Normalizing, it's clear that all search result entities must have a matching FQN. This puts a stronger emphasis on the Build_Searchable_QualifiedName and its ability to standardize the name. --- .../Helpers/PurviewCustomType.cs | 71 +++++++++++-------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs index a98a833..dc47a69 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs @@ -408,50 +408,59 @@ private async Task> SelectReturnEntity(List 0) { - // If there are already some entities, we might want to jump the line if - // this entity is attached to an adf process in any way. - if (validEntities.Count > 0) - { - JObject folder = await _client.GetByGuid(entity.id); - if (IsInputOrOutputOfAzureDataFactoryEntity(folder)){ - _logger.LogDebug($"Validating {this.to_compare_QualifiedName} vs {entity.qualifiedName} - Discovered entity is part of an ADF process and has been inserted first"); - validEntities.Insert(0,entity); - continue; - } + JObject folder = await _client.GetByGuid(entity.id); + if (IsInputOrOutputOfAzureDataFactoryEntity(folder)){ + _logger.LogDebug($"Validating {this.to_compare_QualifiedName} vs {entity.qualifiedName} - Discovered entity is part of an ADF process and has been inserted first"); + validEntities.Insert(0,entity); + continue; } + } + // Fall through: We know the qualified name matches but it's either the first + // valid entity OR it's not attached to any Azure Data Factory process + validEntities.Add(entity); + } + else + { + // Fall through: We know the qualified name matches but it's not any of the above special cases validEntities.Add(entity); } } - return validEntities; + return validEntities; } private string Name_To_Search(string Name) From 2636d404833f88c338a82feb74f1a521370284f1 Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Sat, 11 Feb 2023 21:49:30 -0600 Subject: [PATCH 14/59] Fix spark3-test-def merge conflict --- tests/integration/jobdefs/spark3-tests-def.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/integration/jobdefs/spark3-tests-def.json b/tests/integration/jobdefs/spark3-tests-def.json index 6c811bc..1af96c1 100644 --- a/tests/integration/jobdefs/spark3-tests-def.json +++ b/tests/integration/jobdefs/spark3-tests-def.json @@ -206,11 +206,7 @@ } ], "notebook_task": { -<<<<<<< HEAD "notebook_path": "/Shared/examples/wasbs-in-kusto-out", -======= - "notebook_path": "/Shared/examples/Test Kusto Write", ->>>>>>> e14c399 (Implemented Kusto support and added unit and integration tests) "source": "WORKSPACE" }, "existing_cluster_id": "0104-045638-iaecf5ne", From 45f65db52842304152050910ed02b1cec9186739 Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Sat, 11 Feb 2023 23:13:38 -0600 Subject: [PATCH 15/59] Prioritize blob paths over placeholder entity --- .../src/Function.Domain/Helpers/PurviewCustomType.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs index dc47a69..eff5f83 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs @@ -447,7 +447,12 @@ private async Task> SelectReturnEntity(List Date: Sun, 12 Feb 2023 09:02:58 -0600 Subject: [PATCH 16/59] Refactoring validentity to validEntitiesAfterFiltering to make it more clear that filtering has occured --- .../Helpers/PurviewCustomType.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs index eff5f83..451f409 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs @@ -367,22 +367,22 @@ public async Task QueryInPurview(string TypeName) if (results.Count > 0) { _logger.LogDebug($"Existing Asset Match Search for {_fqn}: The first match has a fqn of {results[0].qualifiedName} and type of {results[0].entityType}"); - List validentity = await SelectReturnEntity(results); - _logger.LogDebug($"Existing Asset Match Search for {_fqn}: Found {validentity.Count} valid entity matches"); - if (validentity.Count > 0) + List validEntitiesAfterFiltering = await SelectReturnEntity(results); + _logger.LogDebug($"Existing Asset Match Search for {_fqn}: Found {validEntitiesAfterFiltering.Count} valid entity matches"); + if (validEntitiesAfterFiltering.Count > 0) { - _logger.LogDebug($"Existing Asset Match Search for {_fqn}: The first valid match has a fqn of {validentity[0].qualifiedName} and type of {validentity[0].entityType}"); - obj = validentity[0]; - properties["guid"] = validentity[0].id; - properties["typeName"] = validentity[0].entityType; - properties!["attributes"]!["qualifiedName"] = validentity[0].qualifiedName; - this.Fullentity = await this._client.GetByGuid(validentity[0].id); + _logger.LogInformation($"Existing Asset Match Search for {_fqn}: The first valid match has a fqn of {validEntitiesAfterFiltering[0].qualifiedName} and type of {validEntitiesAfterFiltering[0].entityType}"); + obj = validEntitiesAfterFiltering[0]; + properties["guid"] = validEntitiesAfterFiltering[0].id; + properties["typeName"] = validEntitiesAfterFiltering[0].entityType; + properties!["attributes"]!["qualifiedName"] = validEntitiesAfterFiltering[0].qualifiedName; + this.Fullentity = await this._client.GetByGuid(validEntitiesAfterFiltering[0].id); this.is_dummy_asset = false; } // If there are matches but there are none that are valid, it should still be a dummy asset else { - _logger.LogDebug($"Existing Asset Match Search for {_fqn}: Changing type to dummy type because zero valid entities"); + _logger.LogInformation($"Existing Asset Match Search for {_fqn}: Changing type to placeholder type because zero valid entities"); properties["typeName"] = EntityType; } } From 61ea2324985eb0df7f327144fba53d504c142dcb Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Sun, 12 Feb 2023 11:16:03 -0600 Subject: [PATCH 17/59] Removing unncessary column mapping comments and Validate_Resource_Set method which is never referenced --- .../parser/DatabricksToPurviewParser.cs | 9 --------- .../Helpers/parser/IColParser.cs | 1 - .../Models/Parser/Purview/BaseAttributes.cs | 2 -- .../Services/PurviewIngestion.cs | 19 ------------------- 4 files changed, 31 deletions(-) diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/DatabricksToPurviewParser.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/DatabricksToPurviewParser.cs index 9ac9bfb..50fe3d9 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/DatabricksToPurviewParser.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/DatabricksToPurviewParser.cs @@ -108,7 +108,6 @@ public DatabricksWorkspace GetDatabricksWorkspace() DatabricksWorkspace databricksWorkspace = new DatabricksWorkspace(); databricksWorkspace.Attributes.Name = $"{_adbWorkspaceUrl}.azuredatabricks.net"; databricksWorkspace.Attributes.QualifiedName = $"databricks://{_adbWorkspaceUrl}.azuredatabricks.net"; - //databricksWorkspace.Attributes.ColumnMapping = JsonConvert.SerializeObject(_colParser.GetColIdentifiers()); return databricksWorkspace; } @@ -294,7 +293,6 @@ public DatabricksProcess GetDatabricksProcess(string taskQn) } databricksProcess.Attributes = GetProcAttributes(taskQn, inputs,outputs,_eEvent.OlEvent); - //databricksProcess.Attributes.ColumnMapping = JsonConvert.SerializeObject(_colParser.GetColIdentifiers()); databricksProcess.RelationshipAttributes.Task.QualifiedName = taskQn; return databricksProcess; } @@ -322,13 +320,6 @@ private InputOutput GetInputOutputs(IInputsOutputs inOut) return inputOutputId; } - // private ColumnLevelAttributes GetColumnLevelAttributes(IInputsOutputs inOut) - // { - // var id = _colParser.GetColIdentifiers(_eEvent.OlEvent.Outputs); - // var columnLevelId = new ColumnLevelAttributes(); - // return columnLevelId; - // } - private string GetInputsOutputsHash(List inputs, List outputs) { inputs.Sort((x, y) => x.UniqueAttributes.QualifiedName.CompareTo(y.UniqueAttributes.QualifiedName));; diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/IColParser.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/IColParser.cs index 2ac9aae..a4def98 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/IColParser.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/IColParser.cs @@ -7,7 +7,6 @@ namespace Function.Domain.Helpers { - //Interface for ColParser.cs public interface IColParser { public List GetColIdentifiers(); diff --git a/function-app/adb-to-purview/src/Function.Domain/Models/Parser/Purview/BaseAttributes.cs b/function-app/adb-to-purview/src/Function.Domain/Models/Parser/Purview/BaseAttributes.cs index 9bb4220..66ea4b5 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Models/Parser/Purview/BaseAttributes.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Models/Parser/Purview/BaseAttributes.cs @@ -12,7 +12,5 @@ public class BaseAttributes public string Name = ""; [JsonProperty("qualifiedName")] public string QualifiedName = ""; - // [JsonProperty("columnMapping")] - // public string ColumnMapping = ""; } } \ No newline at end of file diff --git a/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs b/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs index 0fe5cad..74e0dbe 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs @@ -396,25 +396,6 @@ private async Task Validate_Process_Entities(JObject Process) } return Process; } - private async Task Validate_Resource_Set(string qualifiedName) - { - string[] tmpName = qualifiedName.Split('/'); - string Name = tmpName[tmpName.Length - 1]; - if (Name == "") - Name = tmpName[tmpName.Length - 2]; - string typeName = "azure_datalake_gen2_resource_set"; - PurviewCustomType sourceEntity = new PurviewCustomType(Name - , typeName - , qualifiedName - , typeName - , $"Data Assets {Name}" - , NewGuid() - , _logger - , _purviewClient); - - var outputObj = await sourceEntity.QueryInPurview(); - return sourceEntity; - } private bool Validate_Process_Json(JObject Process) { var _typename = get_attribute("typeName", Process); From d53f45f85d7b64cee6090b4573bdfdda07962c2f Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Sun, 12 Feb 2023 16:18:16 -0600 Subject: [PATCH 18/59] Refactoring PurviewIngestion to remove unused methods and refactor our the Log method in favor of using the ILogger directly --- .../Services/PurviewIngestion.cs | 89 +++++++------------ 1 file changed, 31 insertions(+), 58 deletions(-) diff --git a/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs b/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs index 74e0dbe..07d979e 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs @@ -23,17 +23,13 @@ namespace Function.Domain.Services /// public class PurviewIngestion : IPurviewIngestion { - private bool useResourceSet = bool.Parse(Environment.GetEnvironmentVariable("useResourceSet") ?? "true"); private PurviewClient _purviewClient; private Int64 initGuid = -1000; - //stores all mappings of columns for Origin and destination assets - private Hashtable columnmapping = new Hashtable(); //flag use to mark if a data Asset is a Dummy type private Dictionary entities = new Dictionary(); List inputs_outputs = new List(); private JArray to_purview_Json = new JArray(); private readonly ILogger _logger; - private List found_entities = new List(); private MemoryCache _payLoad = MemoryCache.Default; private AppConfigurationSettings? config = new AppConfigurationSettings(); private CacheItemPolicy cacheItemPolicy; @@ -80,7 +76,7 @@ public async Task SendToPurview(JObject json) if (entities == null) { - Log("Error", "Not found Attribute entities on " + json.ToString()); + _logger.LogError("Not found Attribute entities on " + json.ToString()); return false; } @@ -164,42 +160,42 @@ public async Task SendToPurview(JObject json) } payload = "{\"entities\": " + tempEntities.ToString() + "}"; JObject? Jpayload = JObject.Parse(payload); - Log("Info", $"Input/Output Entities to load: {Jpayload.ToString()}"); + _logger.LogInformation($"Input/Output Entities to load: {Jpayload.ToString()}"); results = await _purviewClient.Send_to_Purview(payload); if (results != null) { if (results.ReasonPhrase != "OK") { - Log("Error", $"Error Loading Input/Outputs to Purview: Return Code: {results.StatusCode} - Reason:{results.ReasonPhrase}"); + _logger.LogError($"Error Loading Input/Outputs to Purview: Return Code: {results.StatusCode} - Reason:{results.ReasonPhrase}"); } else { var data = await results.Content.ReadAsStringAsync(); - Log("Info", $"Purview Loaded Relationship, Input and Output Entities: Return Code: {results.StatusCode} - Reason:{results.ReasonPhrase} - Content: {data}"); + _logger.LogInformation($"Purview Loaded Relationship, Input and Output Entities: Return Code: {results.StatusCode} - Reason:{results.ReasonPhrase} - Content: {data}"); } } else { - Log("Error", $"Error Loading to Purview!"); + _logger.LogError($"Error Loading to Purview!"); } } if (to_purview_Json.Count > 0) { - Log("Debug", to_purview_Json.ToString()); + _logger.LogDebug(to_purview_Json.ToString()); payload = "{\"entities\": " + to_purview_Json.ToString() + "}"; JObject? Jpayload = JObject.Parse(payload); - Log("Info", $"To Purview Json Entities to load: {Jpayload.ToString()}"); + _logger.LogInformation($"To Purview Json Entities to load: {Jpayload.ToString()}"); results = await _purviewClient.Send_to_Purview(payload); if (results != null) { if (results.ReasonPhrase != "OK") { - Log("Error", $"Error Loading to Purview JSON Entiitesto Purview: Return Code: {results.StatusCode} - Reason:{results.ReasonPhrase}"); + _logger.LogError($"Error Loading to Purview JSON Entiitesto Purview: Return Code: {results.StatusCode} - Reason:{results.ReasonPhrase}"); } } else { - Log("Error", $"Error Loading to Purview!"); + _logger.LogError($"Error Loading to Purview!"); } foreach (var entity in this.entities) { @@ -211,12 +207,12 @@ public async Task SendToPurview(JObject json) { if (json.Count > 0) { - Log("INFO", $"Payload: {json}"); - Log("Error", "Nothing found to load on to Purview, look if the payload is empty."); + _logger.LogInformation($"Payload: {json}"); + _logger.LogError("Nothing found to load on to Purview, look if the payload is empty."); } else { - Log("Error", "No Purview entity to load"); + _logger.LogError("No Purview entity to load"); } foreach (var entity in this.entities) { @@ -225,7 +221,7 @@ public async Task SendToPurview(JObject json) return false; } } - Log("INFO", $"Payload already registered in Microsoft Purview: {json.ToString()}"); + _logger.LogInformation($"Payload already registered in Microsoft Purview: {json.ToString()}"); return false; } private bool Validate_Entities_Json(JObject Process) @@ -275,24 +271,34 @@ private async Task Validate_Entities(JObject Process) String proctype = Process["typeName"]!.ToString(); if (sourceEntity.Properties.ContainsKey("typeName")){ String sourcetype = sourceEntity.Properties["typeName"]!.ToString(); - Log("Info", $"PQN:{qualifiedName} Process Type name is {proctype} and sourceEntity original TypeName was {sourcetype}"); + _logger.LogInformation($"PQN:{qualifiedName} Process Type name is {proctype} and sourceEntity original TypeName was {sourcetype}"); }else{ - Log("Info", $"PQN:{qualifiedName} Process Type name is {proctype} and sourceEntity original TypeName was not set"); + _logger.LogInformation($"PQN:{qualifiedName} Process Type name is {proctype} and sourceEntity original TypeName was not set"); } if (sourceEntity.is_dummy_asset) { - Log("Info", "IN DUMMY ASSET AND ABOUT TO OVERWRITE"); + _logger.LogInformation("IN DUMMY ASSET AND ABOUT TO OVERWRITE"); sourceEntity.Properties["typeName"] = Process["typeName"]!.ToString(); if (!entities.ContainsKey(qualifiedName)) entities.Add(qualifiedName, sourceEntity); - Log("Info", $"Entity: {qualifiedName} Type: {typename}, Not found, Creating Dummy Entity"); + _logger.LogInformation($"Entity: {qualifiedName} Type: {typename}, Not found, Creating Dummy Entity"); return sourceEntity; } if (!entities.ContainsKey(qualifiedName)) entities.Add(qualifiedName, sourceEntity); return sourceEntity; } + + /// + /// Transform the provided JSON object (an input or output entity for a Purview process). + /// This entity will have their qualified name and type updated based on searching for + /// an existing entity in the purview instance. + /// In addition the entity is added to the inputs_outputs property of PurviewIngestion. + /// + /// Json Object + /// Should be either 'inputs' or 'outputs' + /// A PurviewCustomType private async Task SetOutputInput(JObject outPutInput, string inorout) { @@ -320,7 +326,7 @@ private async Task SetOutputInput(JObject outPutInput, string outPutInput["uniqueAttributes"]!["qualifiedName"] = sourceEntity.Properties!["attributes"]!["qualifiedName"]!.ToString().ToLower(); inputs_outputs.Add(sourceEntity); - Log("Info", $"{inorout} Entity: {qualifiedName} Type: {typename}, Not found, Creating Dummy Entity"); + _logger.LogInformation($"{inorout} Entity: {qualifiedName} Type: {typename}, Not found, Creating Dummy Entity"); } else { @@ -401,19 +407,19 @@ private bool Validate_Process_Json(JObject Process) var _typename = get_attribute("typeName", Process); if (_typename == null) { - Log("Info", "Not found Attribute typename on " + Process.ToString()); + _logger.LogInformation("Not found Attribute typename on " + Process.ToString()); return false; } var _attributes = get_attribute("attributes", Process); if (!_attributes.HasValues) { - Log("Error", "Not found Attribute attributes on " + Process.ToString()); + _logger.LogError("Not found Attribute attributes on " + Process.ToString()); return false; } if (!((JObject)Process["attributes"]!).ContainsKey("columnMapping")) { - Log("Info", $"Not found Attribute columnMapping on {Process.ToString()} i is not a Process Entity!"); + _logger.LogInformation($"Not found Attribute columnMapping on {Process.ToString()} i is not a Process Entity!"); return false; } @@ -442,31 +448,6 @@ private Int64 NewGuid() return initGuid--; } - - private void Remove_Unused_Dummy_Entitites() - { - foreach (var entity in this.entities) - { - - } - } - - private void Log(string type, string msg) - { - if (type.ToUpper() == "ERROR") - { _logger.LogError(msg); return; } - if (type.ToUpper() == "INFO") - { _logger.LogInformation(msg); return; } - if (type.ToUpper() == "DEBUG") - { _logger.LogDebug(msg); return; } - if (type.ToUpper() == "WARNING") - { _logger.LogWarning(msg); return; } - if (type.ToUpper() == "CRITICAL") - { _logger.LogCritical(msg); return; } - if (type.ToUpper() == "TRACE") - { _logger.LogInformation(msg); return; } - } - private static string CalculateHash(string payload) { var newKey = Encoding.UTF8.GetBytes(payload); @@ -484,12 +465,4 @@ private static string CalculateHash(string payload) } } - /// - /// Enumeration of the Microsoft Purview Process entity relationships - /// - public enum Relationships_Type - { - inputs, - outputs - } } \ No newline at end of file From a9479696a2e80703a27dd71a6c62c9eea9964efc Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Sun, 12 Feb 2023 16:46:28 -0600 Subject: [PATCH 19/59] Refactor Validate_X_Json method names to reflect what it is testing or verifying --- .../Services/PurviewIngestion.cs | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs b/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs index 07d979e..5fc1934 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs @@ -88,16 +88,14 @@ public async Task SendToPurview(JObject json) foreach (JObject entity in entities) { - - - if (Validate_Process_Json(entity)) + if (IsProcessEntity(entity)) { JObject new_entity = await Validate_Process_Entities(entity); to_purview_Json.Add(new_entity); } else { - if (Validate_Entities_Json(entity)) + if (EntityAttributesHaveBeenPopulated(entity)) { PurviewCustomType new_entity = await Validate_Entities(entity); //Check Entity Relatioship @@ -224,24 +222,20 @@ public async Task SendToPurview(JObject json) _logger.LogInformation($"Payload already registered in Microsoft Purview: {json.ToString()}"); return false; } - private bool Validate_Entities_Json(JObject Process) + private bool EntityAttributesHaveBeenPopulated(JObject questionableEntity) { - if (!Process.ContainsKey("typeName")) + if (!questionableEntity.ContainsKey("typeName")) { return false; } - /* if (!Process.ContainsKey("guid")) - { - return false; - }*/ - if (!Process.ContainsKey("attributes")) + if (!questionableEntity.ContainsKey("attributes")) { return false; } - if (Process["attributes"]!.GetType() != typeof(JObject)) + if (questionableEntity["attributes"]!.GetType() != typeof(JObject)) return false; - if (!((JObject)Process["attributes"]!).ContainsKey("qualifiedName")) + if (!((JObject)questionableEntity["attributes"]!).ContainsKey("qualifiedName")) { return false; } @@ -402,7 +396,7 @@ private async Task Validate_Process_Entities(JObject Process) } return Process; } - private bool Validate_Process_Json(JObject Process) + private bool IsProcessEntity(JObject Process) { var _typename = get_attribute("typeName", Process); if (_typename == null) @@ -419,7 +413,7 @@ private bool Validate_Process_Json(JObject Process) if (!((JObject)Process["attributes"]!).ContainsKey("columnMapping")) { - _logger.LogInformation($"Not found Attribute columnMapping on {Process.ToString()} i is not a Process Entity!"); + _logger.LogInformation($"Not found Attribute columnMapping on {Process.ToString()} is not a Process Entity!"); return false; } From f06182625bf52322822c5b92f69497c1ee0ddac2 Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Sun, 12 Feb 2023 17:58:46 -0600 Subject: [PATCH 20/59] Refactoring SendToPurview in PurviewIngestion for each loop's variable from a simple entity to purviewEntityToBeUpdated to make it more clear what's going to happen to this entity and not confuse it with other references to the word 'entity' --- .../Services/PurviewIngestion.cs | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs b/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs index 5fc1934..6a7c9ae 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs @@ -86,36 +86,33 @@ public async Task SendToPurview(JObject json) var cacheItem = new CacheItem(dataEvent, dataEvent); _payLoad.Add(cacheItem, cacheItemPolicy); - foreach (JObject entity in entities) + foreach (JObject purviewEntityToBeUpdated in entities) { - if (IsProcessEntity(entity)) + if (IsProcessEntity(purviewEntityToBeUpdated)) { - JObject new_entity = await Validate_Process_Entities(entity); + JObject new_entity = await Validate_Process_Entities(purviewEntityToBeUpdated); to_purview_Json.Add(new_entity); } else { - if (EntityAttributesHaveBeenPopulated(entity)) + if (EntityAttributesHaveBeenPopulated(purviewEntityToBeUpdated)) { - PurviewCustomType new_entity = await Validate_Entities(entity); - //Check Entity Relatioship - // if (new_entity.is_dummy_asset) - // to_purview_Json.Add(new_entity.Properties); + PurviewCustomType new_entity = await Validate_Entities(purviewEntityToBeUpdated); - string qualifiedName = entity["attributes"]!["qualifiedName"]!.ToString(); - if (entity.ContainsKey("relationshipAttributes")) + string qualifiedName = purviewEntityToBeUpdated["attributes"]!["qualifiedName"]!.ToString(); + if (purviewEntityToBeUpdated.ContainsKey("relationshipAttributes")) { - foreach (var rel in entity["relationshipAttributes"]!.Values()) + foreach (var rel in purviewEntityToBeUpdated["relationshipAttributes"]!.Values()) { - if (((JObject)(entity["relationshipAttributes"]![rel!.Name]!)).ContainsKey("qualifiedName")) + if (((JObject)(purviewEntityToBeUpdated["relationshipAttributes"]![rel!.Name]!)).ContainsKey("qualifiedName")) { - if (this.entities.ContainsKey(entity["relationshipAttributes"]![rel!.Name]!["qualifiedName"]!.ToString())) + if (this.entities.ContainsKey(purviewEntityToBeUpdated["relationshipAttributes"]![rel!.Name]!["qualifiedName"]!.ToString())) { - entity["relationshipAttributes"]![rel!.Name]!["guid"] = this.entities[entity["relationshipAttributes"]![rel!.Name]!["qualifiedName"]!.ToString()].Properties["guid"]; + purviewEntityToBeUpdated["relationshipAttributes"]![rel!.Name]!["guid"] = this.entities[purviewEntityToBeUpdated["relationshipAttributes"]![rel!.Name]!["qualifiedName"]!.ToString()].Properties["guid"]; } else { - string qn = entity["relationshipAttributes"]![rel!.Name]!["qualifiedName"]!.ToString(); + string qn = purviewEntityToBeUpdated["relationshipAttributes"]![rel!.Name]!["qualifiedName"]!.ToString(); PurviewCustomType sourceEntity = new PurviewCustomType("search relationship" , "" , qn @@ -130,18 +127,28 @@ public async Task SendToPurview(JObject json) if (!this.entities.ContainsKey(qn)) this.entities.Add(qn, sourceEntity); - entity["relationshipAttributes"]![rel!.Name]!["guid"] = sourceEntity.Properties["guid"]; + purviewEntityToBeUpdated["relationshipAttributes"]![rel!.Name]!["guid"] = sourceEntity.Properties["guid"]; } } } } - to_purview_Json.Add(entity); + to_purview_Json.Add(purviewEntityToBeUpdated); } } } + + + + + + + + + + HttpResponseMessage results; string? payload = ""; From d3dd7afe448a5a4b4e0c40904e7f53998b685687 Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Sun, 12 Feb 2023 21:30:21 -0600 Subject: [PATCH 21/59] Refactoring PurviewIngestion naming conventions to clarify entities that may be deleted and adding comments to walk through relationship attribute extraction and replacement --- .../Services/PurviewIngestion.cs | 57 +++++++++++-------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs b/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs index 6a7c9ae..8e09b1a 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs @@ -26,7 +26,7 @@ public class PurviewIngestion : IPurviewIngestion private PurviewClient _purviewClient; private Int64 initGuid = -1000; //flag use to mark if a data Asset is a Dummy type - private Dictionary entities = new Dictionary(); + private Dictionary entitiesMarkedForDeletion = new Dictionary(); List inputs_outputs = new List(); private JArray to_purview_Json = new JArray(); private readonly ILogger _logger; @@ -99,36 +99,43 @@ public async Task SendToPurview(JObject json) { PurviewCustomType new_entity = await Validate_Entities(purviewEntityToBeUpdated); - string qualifiedName = purviewEntityToBeUpdated["attributes"]!["qualifiedName"]!.ToString(); if (purviewEntityToBeUpdated.ContainsKey("relationshipAttributes")) { + // For every relationship attribute foreach (var rel in purviewEntityToBeUpdated["relationshipAttributes"]!.Values()) { + // If the relationship attribute has a qualified name property if (((JObject)(purviewEntityToBeUpdated["relationshipAttributes"]![rel!.Name]!)).ContainsKey("qualifiedName")) { - if (this.entities.ContainsKey(purviewEntityToBeUpdated["relationshipAttributes"]![rel!.Name]!["qualifiedName"]!.ToString())) + string _qualifiedNameOfRelatedAsset = purviewEntityToBeUpdated["relationshipAttributes"]![rel!.Name]!["qualifiedName"]!.ToString(); + // If the entitiesMarkedForDeletion dictionary has this related asset + // update the guid of the relationship attribute we're in to be the original one? + if (this.entitiesMarkedForDeletion.ContainsKey(_qualifiedNameOfRelatedAsset)) { - purviewEntityToBeUpdated["relationshipAttributes"]![rel!.Name]!["guid"] = this.entities[purviewEntityToBeUpdated["relationshipAttributes"]![rel!.Name]!["qualifiedName"]!.ToString()].Properties["guid"]; + purviewEntityToBeUpdated["relationshipAttributes"]![rel!.Name]!["guid"] = this.entitiesMarkedForDeletion[_qualifiedNameOfRelatedAsset].Properties["guid"]; } else { - string qn = purviewEntityToBeUpdated["relationshipAttributes"]![rel!.Name]!["qualifiedName"]!.ToString(); + // This entity is created solely to be able to search for the asset based on qualifiedName PurviewCustomType sourceEntity = new PurviewCustomType("search relationship" , "" - , qn + , _qualifiedNameOfRelatedAsset , "" , "search relationship" - , NewGuid() + , NewGuid() // This will be updated after successfully finding the asset via query in purview , _logger , _purviewClient); - + // TODO This should use the qualifiedNamePrefix filter + // Currently fqn may change here QueryValeuModel sourceJson = await sourceEntity.QueryInPurview(); - if (!this.entities.ContainsKey(qn)) - this.entities.Add(qn, sourceEntity); + // If the related asset has not been seen, add it to the list of assets to be deleted? + if (!this.entitiesMarkedForDeletion.ContainsKey(_qualifiedNameOfRelatedAsset)) + this.entitiesMarkedForDeletion.Add(_qualifiedNameOfRelatedAsset, sourceEntity); + // Update the guid of the relationship attribute with the one that was discovered () + // TODO Handle when sourceJson does not return a typed asset purviewEntityToBeUpdated["relationshipAttributes"]![rel!.Name]!["guid"] = sourceEntity.Properties["guid"]; - } } @@ -202,9 +209,9 @@ public async Task SendToPurview(JObject json) { _logger.LogError($"Error Loading to Purview!"); } - foreach (var entity in this.entities) + foreach (var deletableEntity in this.entitiesMarkedForDeletion) { - await _purviewClient.Delete_Unused_Entity(entity.Key, "purview_custom_connector_generic_entity_with_columns"); + await _purviewClient.Delete_Unused_Entity(deletableEntity.Key, "purview_custom_connector_generic_entity_with_columns"); } return true; } @@ -219,9 +226,9 @@ public async Task SendToPurview(JObject json) { _logger.LogError("No Purview entity to load"); } - foreach (var entity in this.entities) + foreach (var deletableEntity in this.entitiesMarkedForDeletion) { - await _purviewClient.Delete_Unused_Entity(entity.Key, "purview_custom_connector_generic_entity_with_columns"); + await _purviewClient.Delete_Unused_Entity(deletableEntity.Key, "purview_custom_connector_generic_entity_with_columns"); } return false; } @@ -281,13 +288,13 @@ private async Task Validate_Entities(JObject Process) { _logger.LogInformation("IN DUMMY ASSET AND ABOUT TO OVERWRITE"); sourceEntity.Properties["typeName"] = Process["typeName"]!.ToString(); - if (!entities.ContainsKey(qualifiedName)) - entities.Add(qualifiedName, sourceEntity); + if (!entitiesMarkedForDeletion.ContainsKey(qualifiedName)) + entitiesMarkedForDeletion.Add(qualifiedName, sourceEntity); _logger.LogInformation($"Entity: {qualifiedName} Type: {typename}, Not found, Creating Dummy Entity"); return sourceEntity; } - if (!entities.ContainsKey(qualifiedName)) - entities.Add(qualifiedName, sourceEntity); + if (!entitiesMarkedForDeletion.ContainsKey(qualifiedName)) + entitiesMarkedForDeletion.Add(qualifiedName, sourceEntity); return sourceEntity; } @@ -335,8 +342,8 @@ private async Task SetOutputInput(JObject outPutInput, string outPutInput["typeName"] = sourceEntity.Properties!["typeName"]!.ToString(); } - if (!entities.ContainsKey(qualifiedName)) - entities.Add(qualifiedName, sourceEntity); + if (!entitiesMarkedForDeletion.ContainsKey(qualifiedName)) + entitiesMarkedForDeletion.Add(qualifiedName, sourceEntity); return sourceEntity; } @@ -377,7 +384,7 @@ private async Task Validate_Process_Entities(JObject Process) string[] tmpName = qualifiedName.Split('/'); Name = tmpName[tmpName.Length - 1]; typename = "purview_custom_connector_generic_entity_with_columns"; - if (!entities.ContainsKey(qualifiedName)) + if (!entitiesMarkedForDeletion.ContainsKey(qualifiedName)) { PurviewCustomType sourceEntity = new PurviewCustomType(Name @@ -392,12 +399,12 @@ private async Task Validate_Process_Entities(JObject Process) var outputObj = await sourceEntity.QueryInPurview(); Process["relationshipAttributes"]![rel!.Name]!["guid"] = sourceEntity.Properties["guid"]; - if (!entities.ContainsKey(qualifiedName)) - entities.Add(qualifiedName, sourceEntity); + if (!entitiesMarkedForDeletion.ContainsKey(qualifiedName)) + entitiesMarkedForDeletion.Add(qualifiedName, sourceEntity); } else { - Process["relationshipAttributes"]![rel!.Name]!["guid"] = entities[qualifiedName].Properties["guid"]; + Process["relationshipAttributes"]![rel!.Name]!["guid"] = entitiesMarkedForDeletion[qualifiedName].Properties["guid"]; } } } From d732ef7e3363a25d1046e9f97ebddf5fe7185243 Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Sun, 12 Feb 2023 22:20:24 -0600 Subject: [PATCH 22/59] Adding a field for originalQualifiedName and removing unused methods simpleEntity, AddToTable, FindQualifiedNameInPurview, CleanUnusedCustomEntities, and Name_To_Search --- .../Helpers/PurviewCustomType.cs | 142 +----------------- 1 file changed, 3 insertions(+), 139 deletions(-) diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs index 451f409..e222c03 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs @@ -25,11 +25,11 @@ public class PurviewCustomType private readonly ILogger _logger; private readonly string EntityType = "purview_custom_connector_generic_entity_with_columns"; private PurviewClient _client; - private JObject? simpleEntity; private JObject? properties; private AppConfigurationSettings? config = new AppConfigurationSettings(); public JObject? Fullentity = new JObject(); bool useResourceSet = true; + public string? originalQualifiedName {get; private set;} /// /// Property that contains all Json attributes for the Custom data Entity in Microsoft Purview /// @@ -139,7 +139,7 @@ private void Init(string name, string typeName, string qualified_name, string da { is_dummy_asset = false; properties = new JObject(); - simpleEntity = new JObject(); + originalQualifiedName = qualified_name; //Loading Entity properties into Json format of an AtlasEntity properties.Add("typeName", typeName); properties.Add("guid", guid); @@ -150,23 +150,7 @@ private void Init(string name, string typeName, string qualified_name, string da ((JObject)properties["attributes"]!).Add("data_type", data_type); ((JObject)properties["attributes"]!).Add("description", description); } - /// - /// Export basic data entity attribute - /// - /// Json Object - public JObject SimpleEntity - { - get - { - //Simple Json format for use in Atlas requests - simpleEntity = new JObject(); - simpleEntity.Add("typeName", this.Properties["typeName"]!.ToString()); - simpleEntity.Add("guid", this.Properties["guid"]!.ToString()); - simpleEntity.Add("qualifiedName", this.Properties["attributes"]!["qualifiedName"]!.ToString()); - _logger.LogInformation($"Retrived Entity simple Object: {simpleEntity.ToString()}"); - return simpleEntity; - } - } + /// /// Get a list on Data Entities in Microsoft Purview using Entity Qualified Name and Type /// @@ -184,84 +168,6 @@ public async Task> GetEntity() /// /// Boolean public bool is_dummy_asset { get; set; } - /// - /// Add Relationship to entity as columns to tables type - /// - /// Table to be related to - /// bool - public bool AddToTable(PurviewCustomType Table) - { - //Validating if the table attribute exists if not we will initialize - if (!((JObject)properties!["relationshipAttributes"]!).ContainsKey("table")) - ((JObject)properties!["relationshipAttributes"]!).Add("table", new JObject()); - - _logger.LogInformation($"Entity qualifiedName: {properties["attributes"]!["qualifiedName"]}Table.simpleEntity: {Table!.simpleEntity!.ToString()}"); - properties["relationshipAttributes"]!["table"] = Table.simpleEntity; - return true; - } - /// - /// Search an Entity in Microsoft Purview using Qualified Name and Type - /// - /// Type to search for - /// Json object - public async Task FindQualifiedNameInPurview(string typeName) - { - //Search using search method and qualifiedName attribute. Scape needs to be used on some non safe (web URLs) chars - EntityModel results = await this._client.search_entities( - properties!["attributes"]!["qualifiedName"]!.ToString() - , typeName); - - if (results.qualifiedName == null) - { - _logger.LogInformation($"Entity qualifiedName:{properties["attributes"]!["qualifiedName"]!.ToString()} - typeName:{typeName}, not found!"); - this.is_dummy_asset = true; - properties["typeName"] = EntityType; - return new JObject(); - } - var guid = ""; - this.is_dummy_asset = false; - var _qualifiedName = ""; - //validate if qualifiedName is the same - _qualifiedName = results.qualifiedName; - if (results.entityType == "azure_datalake_gen2_resource_set") - properties["attributes"]!["qualifiedName"] = _qualifiedName; - if (_qualifiedName.Trim('/').ToLower() == properties["attributes"]!["qualifiedName"]!.ToString().Trim('/').ToLower()) - { - //search api return quid on ID property - guid = results.id; - properties["typeName"] = results.entityType; - properties!["attributes"]!["qualifiedName"] = results.qualifiedName; - //break if find a non dummy entity with the qualified name - if (results.entityType == EntityType) - { - //log.debug("search_entity_by_qualifiedName: entity \'{name}\' is Dummy Entity"); - //mark entity as dummy to be created - this.is_dummy_asset = true; - } - _logger.LogInformation($"Entity qualifiedName:{properties["attributes"]!["qualifiedName"]!.ToString()} - typeName:{typeName} - guid:{guid}, found!"); - } - - if (!this.is_dummy_asset) - properties!["attributes"]!["qualifiedName"] = results.qualifiedName; - - var content = new JObject(); - content.Add(_qualifiedName, new JObject()); - properties["guid"] = guid; - ((JObject)content[_qualifiedName]!).Add("guid", guid); - ((JObject)content[_qualifiedName]!).Add("qualifiedName", properties!["attributes"]!["qualifiedName"]!.ToString()); - - return content; - } - /// - /// Remove any unused Dummy entities - /// - /// Boolean - public async Task CleanUnusedCustomEntities() - { - return await this._client.Delete_Unused_Entity( - properties!["attributes"]!["qualifiedName"]!.ToString() - , properties!["typeName"]!.ToString()); - } private List? qNames = new List(); /// @@ -468,48 +374,6 @@ private async Task> SelectReturnEntity(List isNumber = delegate (string num) - { - return Int64.TryParse(num, out long number)!; - }; - - Func newName = delegate (char separator) - { - string[] partsName = Name.Split(separator); - int index = 0; - foreach (string? part in partsName) - { - if (isNumber(part)) - partsName[index] = "{N}"; - index++; - } - return string.Join(separator, partsName)!; - }; - - if (isNumber(Name)) - { - return "{N}"; - } - - if (Name.Contains('=')) - { - return newName('='); - } - - if (Name.Contains('-')) - { - return newName('-'); - } - - if (Name.Contains('_')) - { - return newName('_'); - } - - return Name; - } private bool Is_Valid_Name(string name) { Func isNumber = delegate (string num) From 48dc0c67bc36bf1ce59f7f02501e25b89d9ebb2f Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Sun, 12 Feb 2023 22:38:44 -0600 Subject: [PATCH 23/59] Refactoring the cache naming conventions inside of PurviewIngestion --- .../Function.Domain/Services/PurviewIngestion.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs b/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs index 8e09b1a..f20913f 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs @@ -30,7 +30,7 @@ public class PurviewIngestion : IPurviewIngestion List inputs_outputs = new List(); private JArray to_purview_Json = new JArray(); private readonly ILogger _logger; - private MemoryCache _payLoad = MemoryCache.Default; + private MemoryCache _cacheOfSeenEvents = MemoryCache.Default; private AppConfigurationSettings? config = new AppConfigurationSettings(); private CacheItemPolicy cacheItemPolicy; /// @@ -72,21 +72,22 @@ public async Task SendToPurview(JArray Processes) /// Boolean public async Task SendToPurview(JObject json) { - var entities = get_attribute("entities", json); + var entitiesFromInitialJson = get_attribute("entities", json); - if (entities == null) + if (entitiesFromInitialJson == null) { _logger.LogError("Not found Attribute entities on " + json.ToString()); return false; } - string ? dataEvent = CalculateHash(entities.ToString()); - if (!_payLoad.Contains(dataEvent)) + // This hash and cache helps to prevent processing the same event multiple times + string ? dataEvent = CalculateHash(entitiesFromInitialJson.ToString()); + if (!_cacheOfSeenEvents.Contains(dataEvent)) { var cacheItem = new CacheItem(dataEvent, dataEvent); - _payLoad.Add(cacheItem, cacheItemPolicy); + _cacheOfSeenEvents.Add(cacheItem, cacheItemPolicy); - foreach (JObject purviewEntityToBeUpdated in entities) + foreach (JObject purviewEntityToBeUpdated in entitiesFromInitialJson) { if (IsProcessEntity(purviewEntityToBeUpdated)) { From 50b0bf5d211a2684fe35622dc11ebcc3f29fc5bc Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Sun, 12 Feb 2023 22:57:51 -0600 Subject: [PATCH 24/59] ColParser should optionally take in a mapping of original dataset names to fully qualified names discovered during search and PurviewIngestion needs to keep track of these mappings --- .../Helpers/PurviewCustomType.cs | 7 +++++++ .../Helpers/parser/ColParser.cs | 19 ++++++++++++++++--- .../Services/PurviewIngestion.cs | 10 ++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs index e222c03..af8c97f 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/PurviewCustomType.cs @@ -38,6 +38,13 @@ public JObject Properties get { return properties!; } } /// + /// Get the current qualifedName + /// + public string currentQualifiedName() + { + return properties!["attributes"]!["qualifiedName"]!.ToString(); + } + /// /// Creation of a Microsoft Purview Custom Type entity that initialize all attributes needed /// /// Name of the Data Entity diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/ColParser.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/ColParser.cs index ec24d84..a4defd3 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/ColParser.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/ColParser.cs @@ -38,8 +38,12 @@ public ColParser(ParserSettings configuration, ILoggerFactory logger, Event olEv /// This class will be used for the parsing code. /// /// - public List GetColIdentifiers() + { + return GetColIdentifiers(new Dictionary{}); + } + + public List GetColIdentifiers(Dictionary originalToMatchedFqn ) { var col = new List(); @@ -48,8 +52,12 @@ public List GetColIdentifiers() foreach(KeyValuePair colInfo in colId.Facets.ColFacets.fields) { var dataSet = new DatasetMappingClass(); - //dataSet.sink = $"{colId.NameSpace}, {colId.Name}"; dataSet.sink = _qnParser.GetIdentifiers(colId.NameSpace, colId.Name).QualifiedName; + // The identifier from OpenLineage may not be the same as what is discovered on + // the Purview catalog. This includes cases like resource sets or blob vs dfs paths + if (originalToMatchedFqn.ContainsKey(dataSet.sink)){ + dataSet.sink = originalToMatchedFqn[dataSet.sink]; + } var columnLevels = new List(); foreach (ColumnLineageIdentifierClass colInfo2 in colInfo.Value.inputFields) { @@ -63,7 +71,12 @@ public List GetColIdentifiers() } } dataSet.source = _qnParser.GetIdentifiers(colInfo2.nameSpace, colInfo2.name).QualifiedName; - //dataSet.source = "*"; + // The identifier from OpenLineage may not be the same as what is discovered on + // the Purview catalog. This includes cases like resource sets or blob vs dfs paths + if (originalToMatchedFqn.ContainsKey(dataSet.source)){ + dataSet.source = originalToMatchedFqn[dataSet.source]; + } + columnLevel.source = colInfo2.field; columnLevel.sink = colInfo.Key; columnLevels.Add(columnLevel); diff --git a/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs b/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs index f20913f..40422cc 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs @@ -27,6 +27,7 @@ public class PurviewIngestion : IPurviewIngestion private Int64 initGuid = -1000; //flag use to mark if a data Asset is a Dummy type private Dictionary entitiesMarkedForDeletion = new Dictionary(); + private Dictionary originalFqnToDiscoveredFqn = new Dictionary(); List inputs_outputs = new List(); private JArray to_purview_Json = new JArray(); private readonly ILogger _logger; @@ -274,6 +275,9 @@ private async Task Validate_Entities(JObject Process) QueryValeuModel sourceJson = await sourceEntity.QueryInPurview(); + // Capture the updated qualified name mapping in case column mapping needs it + originalFqnToDiscoveredFqn[qualifiedName] = sourceEntity.currentQualifiedName(); + Process["guid"] = sourceEntity.Properties["guid"]; @@ -329,6 +333,9 @@ private async Task SetOutputInput(JObject outPutInput, string , _purviewClient); QueryValeuModel sourceJson = await sourceEntity.QueryInPurview(); + // Capture the updated qualified name mapping in case column mapping needs it + originalFqnToDiscoveredFqn[qualifiedName] = sourceEntity.currentQualifiedName(); + if (sourceEntity.is_dummy_asset) { outPutInput["typeName"] = sourceEntity.Properties["typeName"]; @@ -399,6 +406,9 @@ private async Task Validate_Process_Entities(JObject Process) var outputObj = await sourceEntity.QueryInPurview(); + // Capture the updated qualified name mapping in case column mapping needs it + originalFqnToDiscoveredFqn[qualifiedName] = sourceEntity.currentQualifiedName(); + Process["relationshipAttributes"]![rel!.Name]!["guid"] = sourceEntity.Properties["guid"]; if (!entitiesMarkedForDeletion.ContainsKey(qualifiedName)) entitiesMarkedForDeletion.Add(qualifiedName, sourceEntity); From 48af6307dfd240a6175aeaa8e4970ab2891605f3 Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Tue, 14 Feb 2023 01:12:52 -0600 Subject: [PATCH 25/59] Refactoring to support extracting the ColumnParser to be passed around in PurviewOut --- .../Helpers/parser/DatabricksToPurviewParser.cs | 5 +++++ .../Helpers/parser/IDatabricksToPurviewParser.cs | 1 + .../Services/IOlToPurviewParsingService.cs | 3 ++- .../Services/OlToPurviewParsingService.cs | 4 +--- .../src/Function.Domain/Services/PurviewIngestion.cs | 1 + .../adb-to-purview/src/Functions/PurviewOut.cs | 10 ++++++++-- 6 files changed, 18 insertions(+), 6 deletions(-) diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/DatabricksToPurviewParser.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/DatabricksToPurviewParser.cs index 50fe3d9..d3e8c2b 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/DatabricksToPurviewParser.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/DatabricksToPurviewParser.cs @@ -357,5 +357,10 @@ private string GenerateMd5Hash(string input) } return sOutput.ToString(); } + + public IColParser GetColumnParser() + { + return this._colParser; + } } } \ No newline at end of file diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/IDatabricksToPurviewParser.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/IDatabricksToPurviewParser.cs index 409d4b7..114324b 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/IDatabricksToPurviewParser.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/IDatabricksToPurviewParser.cs @@ -17,5 +17,6 @@ public interface IDatabricksToPurviewParser public DatabricksSparkJarTask GetDatabricksSparkJarTask(string jobQn); public DatabricksProcess GetDatabricksProcess(string taskQn); public JobType GetJobType(); + public IColParser GetColumnParser(); } } \ No newline at end of file diff --git a/function-app/adb-to-purview/src/Function.Domain/Services/IOlToPurviewParsingService.cs b/function-app/adb-to-purview/src/Function.Domain/Services/IOlToPurviewParsingService.cs index 37fbce4..8c52348 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Services/IOlToPurviewParsingService.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Services/IOlToPurviewParsingService.cs @@ -2,12 +2,13 @@ // Licensed under the MIT License. using System.Threading.Tasks; +using Function.Domain.Helpers; using Function.Domain.Models.OL; namespace Function.Domain.Services { public interface IOlToPurviewParsingService { - public string? GetPurviewFromOlEvent(EnrichedEvent eventData); + public string? GetPurviewFromOlEvent(EnrichedEvent eventData, IDatabricksToPurviewParser parser); } } \ No newline at end of file diff --git a/function-app/adb-to-purview/src/Function.Domain/Services/OlToPurviewParsingService.cs b/function-app/adb-to-purview/src/Function.Domain/Services/OlToPurviewParsingService.cs index b8c6199..2d74847 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Services/OlToPurviewParsingService.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Services/OlToPurviewParsingService.cs @@ -42,7 +42,7 @@ public OlToPurviewParsingService(ILoggerFactory loggerFactory, IConfiguration co /// /// Contains OpenLineage and, optionally data obtained from the ADB Jobs API /// Serialized Atlas entities - public string? GetPurviewFromOlEvent(EnrichedEvent eventData) + public string? GetPurviewFromOlEvent(EnrichedEvent eventData, IDatabricksToPurviewParser parser) { if (!verifyEventData(eventData)) { @@ -50,8 +50,6 @@ public OlToPurviewParsingService(ILoggerFactory loggerFactory, IConfiguration co return null; } - IDatabricksToPurviewParser parser = new DatabricksToPurviewParser(_loggerFactory, _config, eventData); - if (eventData.IsInteractiveNotebook) { return ParseInteractiveNotebook(parser); diff --git a/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs b/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs index 40422cc..93793cf 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs @@ -93,6 +93,7 @@ public async Task SendToPurview(JObject json) if (IsProcessEntity(purviewEntityToBeUpdated)) { JObject new_entity = await Validate_Process_Entities(purviewEntityToBeUpdated); + // Todo Update Column mapping attribute based on the dictionary and inject the column parser with the openlineage event to_purview_Json.Add(new_entity); } else diff --git a/function-app/adb-to-purview/src/Functions/PurviewOut.cs b/function-app/adb-to-purview/src/Functions/PurviewOut.cs index cc24cdd..263920c 100644 --- a/function-app/adb-to-purview/src/Functions/PurviewOut.cs +++ b/function-app/adb-to-purview/src/Functions/PurviewOut.cs @@ -8,6 +8,8 @@ using Function.Domain.Services; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Function.Domain.Helpers; +using Microsoft.Extensions.Configuration; namespace AdbToPurview.Function { @@ -18,8 +20,9 @@ public class PurviewOut private readonly IOlConsolodateEnrich _olConsolodateEnrich; private readonly IOlToPurviewParsingService _olToPurviewParsingService; private readonly IPurviewIngestion _purviewIngestion; + private readonly IConfiguration _configuration; - public PurviewOut(ILogger logger, IOlToPurviewParsingService olToPurviewParsingService, IPurviewIngestion purviewIngestion, IOlConsolodateEnrich olConsolodateEnrich, ILoggerFactory loggerFactory) + public PurviewOut(ILogger logger, IOlToPurviewParsingService olToPurviewParsingService, IPurviewIngestion purviewIngestion, IOlConsolodateEnrich olConsolodateEnrich, ILoggerFactory loggerFactory, IConfiguration configuration) { logger.LogInformation("Enter PurviewOut"); _logger = logger; @@ -27,6 +30,7 @@ public PurviewOut(ILogger logger, IOlToPurviewParsingService olToPur _olConsolodateEnrich = olConsolodateEnrich; _olToPurviewParsingService = olToPurviewParsingService; _purviewIngestion = purviewIngestion; + _configuration = configuration; } [Function("PurviewOut")] @@ -42,7 +46,9 @@ public async Task Run( _logger.LogInformation($"Start event, duplicate event, or no context found - eventData: {input}"); return ""; } - var purviewEvent = _olToPurviewParsingService.GetPurviewFromOlEvent(enrichedEvent); + + IDatabricksToPurviewParser parser = new DatabricksToPurviewParser(_loggerFactory, _configuration, enrichedEvent); + var purviewEvent = _olToPurviewParsingService.GetPurviewFromOlEvent(enrichedEvent, parser); if (purviewEvent == null) { _logger.LogWarning("No Purview Event found"); From ada23c9237d58d9e797596512641bf3d3e71754c Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Tue, 14 Feb 2023 01:28:40 -0600 Subject: [PATCH 26/59] Implement ColumnParser injection and update column mappings of process entities --- .../src/Function.Domain/Helpers/parser/IColParser.cs | 1 + .../src/Function.Domain/Services/IPurviewIngestion.cs | 5 +++-- .../src/Function.Domain/Services/PurviewIngestion.cs | 11 +++++++---- .../adb-to-purview/src/Functions/PurviewOut.cs | 2 +- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/IColParser.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/IColParser.cs index a4def98..977fb84 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/IColParser.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/IColParser.cs @@ -10,5 +10,6 @@ namespace Function.Domain.Helpers public interface IColParser { public List GetColIdentifiers(); + public List GetColIdentifiers(Dictionary originalToMatchedFqn); } } \ No newline at end of file diff --git a/function-app/adb-to-purview/src/Function.Domain/Services/IPurviewIngestion.cs b/function-app/adb-to-purview/src/Function.Domain/Services/IPurviewIngestion.cs index eb75157..5c44432 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Services/IPurviewIngestion.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Services/IPurviewIngestion.cs @@ -3,12 +3,13 @@ using System.Threading.Tasks; using Newtonsoft.Json.Linq; +using Function.Domain.Helpers; namespace Function.Domain.Services { public interface IPurviewIngestion { - public Task SendToPurview(JArray Processes); - public Task SendToPurview(JObject json); + public Task SendToPurview(JArray Processes, IColParser colParser); + public Task SendToPurview(JObject json, IColParser colParser); } } \ No newline at end of file diff --git a/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs b/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs index 93793cf..d501d1f 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs @@ -14,6 +14,7 @@ using System.Security.Cryptography; using System.Runtime.Caching; using Function.Domain.Models.Settings; +using Newtonsoft.Json; namespace Function.Domain.Services { @@ -54,12 +55,12 @@ public PurviewIngestion(ILogger log) /// /// Array of Entities /// Array on Entities - public async Task SendToPurview(JArray Processes) + public async Task SendToPurview(JArray Processes, IColParser colParser) { foreach (JObject process in Processes) { - if (await SendToPurview(process)) + if (await SendToPurview(process, colParser)) { return new JArray(); } @@ -71,7 +72,7 @@ public async Task SendToPurview(JArray Processes) /// /// Json Object /// Boolean - public async Task SendToPurview(JObject json) + public async Task SendToPurview(JObject json, IColParser colParser) { var entitiesFromInitialJson = get_attribute("entities", json); @@ -93,7 +94,9 @@ public async Task SendToPurview(JObject json) if (IsProcessEntity(purviewEntityToBeUpdated)) { JObject new_entity = await Validate_Process_Entities(purviewEntityToBeUpdated); - // Todo Update Column mapping attribute based on the dictionary and inject the column parser with the openlineage event + // Update Column mapping attribute based on the dictionary and inject the column parser with the openlineage event + string columnMapping = JsonConvert.SerializeObject(colParser.GetColIdentifiers(originalFqnToDiscoveredFqn)); + new_entity["attributes"]!["columnMapping"] = columnMapping; to_purview_Json.Add(new_entity); } else diff --git a/function-app/adb-to-purview/src/Functions/PurviewOut.cs b/function-app/adb-to-purview/src/Functions/PurviewOut.cs index 263920c..7a7588e 100644 --- a/function-app/adb-to-purview/src/Functions/PurviewOut.cs +++ b/function-app/adb-to-purview/src/Functions/PurviewOut.cs @@ -58,7 +58,7 @@ public async Task Run( _logger.LogInformation($"PurviewOut-ParserService: {purviewEvent}"); var jObjectPurviewEvent = JsonConvert.DeserializeObject(purviewEvent) ?? new JObject(); _logger.LogInformation("Calling SendToPurview"); - await _purviewIngestion.SendToPurview(jObjectPurviewEvent); + await _purviewIngestion.SendToPurview(jObjectPurviewEvent, parser.GetColumnParser()); return $"Output message created at {DateTime.Now}"; } From 0c68b4a963e9bce7debf4a90095ddfc9099ac798 Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Tue, 14 Feb 2023 10:56:53 -0600 Subject: [PATCH 27/59] Updating Limitations and Readme to better reflect current state and support of column lineage --- LIMITATIONS.md | 24 +++++++++++++----- README.md | 22 ++++++++-------- assets/img/readme/browse_assets.png | Bin 36568 -> 61123 bytes assets/img/readme/databricks_task_related.png | Bin 52467 -> 102820 bytes assets/img/readme/lineage.png | Bin 35542 -> 43318 bytes assets/img/readme/lineage_view.png | Bin 66968 -> 124015 bytes .../Services/PurviewIngestion.cs | 11 +------- 7 files changed, 30 insertions(+), 27 deletions(-) diff --git a/LIMITATIONS.md b/LIMITATIONS.md index 0506c70..d32b918 100644 --- a/LIMITATIONS.md +++ b/LIMITATIONS.md @@ -10,13 +10,11 @@ The solution accelerator supports a limited set of data sources to be ingested i * [Azure Synapse SQL Pools](#azure-synapse-sql-pools) * [Azure SQL DB](#azure-sql-db) * [Delta Lake](#delta-lake-file-format) -<<<<<<< HEAD * [Azure MySQL](#azure-mysql) * [PostgreSQL](#postgresql) -======= * [Azure Data Explorer](#azure-data-explorer) ->>>>>>> e14c399 (Implemented Kusto support and added unit and integration tests) * [Other Data Sources and Limitations](#other-data-sources-and-limitations) +* [Column Level Mapping Supported Sources](#column-level-mapping-supported-sources) ## Connecting to Assets in Purview @@ -80,6 +78,7 @@ Supports [Delta File Format](https://delta.io/). ## Azure MySQL Supports Azure MySQL through [JDBC](https://learn.microsoft.com/en-us/azure/databricks/external-data/jdbc). + ## PostgreSQL Supports both Azure PostgreSQL and on-prem/VM installations of PostgreSQL through [JDBC](https://learn.microsoft.com/en-us/azure/databricks/external-data/jdbc). @@ -89,6 +88,7 @@ Supports both Azure PostgreSQL and on-prem/VM installations of PostgreSQL throug * This can be corrected by specifying the database schema in the Spark job. * Default configuration supports using multiple strings divided by dots to define a custom schema. For example ```myschema.mytable```. * If you register and scan your postgres server as `localhost` in Microsoft Purview, but use the IP within the Databricks notebook, the assets will not be matched correctly. You need to use the IP when registering the Postgres server. + ## Azure Data Explorer Supports Azure Data Explorer (aka Kusto) through the [Azure Data Explorer Connector for Apache Spark](https://learn.microsoft.com/en-us/azure/data-explorer/spark-connector) @@ -109,10 +109,6 @@ Microsoft Purview's Fully Qualified Names are case sensitive. Spark Jobs may hav As a result, this solution attempts to find the best matching *existing* asset. If no existing asset is found to match based on qualified name, the data source name as found in the Spark query will be used toe create a dummy asset. On a subsequent scan of the data source in Purview and another run of the Spark query with the connector enabled will resolve the linkage. -### Column Level Mapping - -The solution currently does not provide column level mapping within the Microsoft Purview lineage tab. - ### Data Factory The solution currently reflects the unfriendly job name provided by Data Factory to Databricks as noted in [issue 72](https://github.com/microsoft/Purview-ADB-Lineage-Solution-Accelerator/issues/72#issuecomment-1211202405). You will see jobs with names similar to `ADF____`. @@ -142,3 +138,17 @@ The solution supports Spark 3.0, 3.1, 3.2, and 3.3 interactive and job clusters. ### Private Endpoints on Microsoft Purview Currently, the solution does not support pushing lineage to a Private Endpoint backed Microsoft Purview service. The solution may be customized to deploy the Azure Function to connect to Microsoft Purview. Consider reviewing the documentation to [Connect privately and securely to your Microsoft Purview account](https://docs.microsoft.com/en-us/azure/purview/catalog-private-link-account-portal). + +## Column Level Mapping Supported Sources + +Starting with OpenLineage 0.18.0 and release 2.3.0 of the solution accelerator, we support emitting column level mapping from the following sources and their combinations: + +* Read / Write to ABFSS file paths (mount or explicit path `abfss://`) +* Read / Write to WASBS file paths (mount or explicit path `wasbs://`) +* Read / Write to the default metastore in Azure Databricks + * Does NOT support custom hive metastores + +### Column Mapping Support for Delta Format + +* Delta Merge statements are supported when the table is stored in the default metastore +* Delta to Delta is NOT supported at this time diff --git a/README.md b/README.md index 059e952..4dc92e3 100644 --- a/README.md +++ b/README.md @@ -48,13 +48,17 @@ Gathering lineage data is performed in the following steps: * Supports table level lineage from Spark Notebooks and jobs for the following data sources: * Azure SQL - * Azure Synapse Analytics + * Azure Synapse Analytics (as input) * Azure Data Lake Gen 2 * Azure Blob Storage * Delta Lake + * Azure Data Explorer + * MySQL + * PostgreSQL * Supports Spark 3.0, 3.1, 3.2, and 3.3 (Interactive and Job clusters) / Spark 2.x (Job clusters) * Databricks Runtimes between 9.1 and 11.3 LTS are currently supported -* Can be configured per cluster or for all clusters as a global configuration +* Can be configured per cluster or for all clusters as a global configuration +* Support **column level lineage** for ABFSS, WASBS, and default metastore hive tables (see [Limitations](./LIMITATIONS.md#column-level-mapping-supported-sources) for more detail) * Once configured, **does not require any code changes to notebooks or jobs** * Can [add new source support through configuration](./docs/extending-source-support.md) @@ -92,26 +96,24 @@ There are two deployment options for this solution accelerator: 1. Once complete, open your Purview workspace and click the "Browse assets" button near the center of the page 1. Click on the "By source type" tab -You should see several items listed under the heading of "Custom source types". There will be a Databricks section and possibly a Purview Custom Connector section under this heading +You should see at least one item listed under the heading of "Azure Databricks". In addition there will possibly be a Purview Custom Connector section under the Custom source types heading ![browse_assets.png](./assets/img/readme/browse_assets.png) -1. Click on the "Databricks" section, then click on the "Databricks Notebook" tile which corresponds to the notebook you ran. In the Properties or Related tabs select one of the "Notebook Tasks" which represent a task in a Databricks job. From the "Databricks Notebook Task", you may see the lineage of one or many of the different spark actions in the notebook. This application may have a number of "Databricks Processes" linked under it which represent the data lineage. To see these, see the Properties or Related tabs +1. Click on the "Databricks" section, then click on the link to the Azure Databricks workspace which the sample notebook was ran. Then select the notebook which you ran (for those running Databricks Jobs, you can also select the job and drill into the related tasks) + * After running a Databricks Notebook on an Interactive Cluster, you will see lineage directly in the Notebook asset under the Lineage tab. + * After running a Databricks Job on a Job Cluster, you will see lineage in the Notebook Task asset. To navigate from a Notebook to a Notebook Task select the Properties tab and choose the Notebook Tasks from the Related Assets section. Please note that Databricks Jobs lineage require [additional setup](./deploy-base.md#support-extracting-lineage-from-databricks-jobs) outside of the demo deployment. ![databricks_task_related.png](./assets/img/readme/databricks_task_related.png) -1. From the Related view, click on the processes icon, then click on one of the links representing the associated process objects - -1. Click on the properties tab to view the properties associated with the process. Note that the full Spark Plan is included - - ![spark_plan.png](./assets/img/readme/spark_plan.png) - 1. Click to the lineage view to see the lineage graph ![lineage_view.png](./assets/img/readme/lineage_view.png) **Note**: If you are viewing the Databricks Process shortly after it was created, sometimes the lineage tab takes some time to display. If you do not see the lineage tab, wait a few minutes and then refresh the browser. + **Lineage Note**: The screenshot above shows lineage to an Azure Data Lake Gen 2 folder, you must have scanned your Data Lake prior to running a notebook for it to be able to match to a Microsoft Purview built-in type like folders or resource sets. + ## Troubleshooting **When filing a new issue, [please include associated log message(s) from Azure Functions](./TROUBLESHOOTING.md#debug-logs).** This will allow the core team to debug within our test environment to validate the issue and develop a solution. diff --git a/assets/img/readme/browse_assets.png b/assets/img/readme/browse_assets.png index f65459ce2f0a7ce4226882a6c8b72fa2b8e6fa25..fac9359e142de0a6f625e8c5e5f39aaa65b3f7e4 100644 GIT binary patch literal 61123 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 He$R~0w literal 36568 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�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 HGw=Ta1*tYm diff --git a/assets/img/readme/databricks_task_related.png b/assets/img/readme/databricks_task_related.png index c45ef732a52e8f6d17b937a6d239702edb5d6c35..6368d9f2cdc7eaff5164e8fdce93b0e8a5b1731a 100644 GIT binary patch literal 102820 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¯Xc{WDA&#}M_Zl{UbZ+g^ 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 literal 52467 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 SQz*)ao@*L<>J^u7h5i%n(z9Iv literal 35542 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

Ic8Sl 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

6lE70LenQoAqpmVrb| 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

xOZsj98){QuV*W?=~M<#5V=N9H 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 literal 66968 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 diff --git a/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs b/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs index d501d1f..a121765 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Services/PurviewIngestion.cs @@ -95,6 +95,7 @@ public async Task SendToPurview(JObject json, IColParser colParser) { JObject new_entity = await Validate_Process_Entities(purviewEntityToBeUpdated); // Update Column mapping attribute based on the dictionary and inject the column parser with the openlineage event + // This lets us use the discovered inputs / outputs rather than just what open lineage provides. string columnMapping = JsonConvert.SerializeObject(colParser.GetColIdentifiers(originalFqnToDiscoveredFqn)); new_entity["attributes"]!["columnMapping"] = columnMapping; to_purview_Json.Add(new_entity); @@ -152,16 +153,6 @@ public async Task SendToPurview(JObject json, IColParser colParser) } } - - - - - - - - - - HttpResponseMessage results; string? payload = ""; From eddfa25d96de6942be14bf33767d06f355f25c4f Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Wed, 22 Feb 2023 15:42:16 -0600 Subject: [PATCH 28/59] Handle Azure Data Factory Job Names (#137) Truncate Azure Data Factory job name guid to prevent creating duplicate jobs / tasks only differntiated by a guid / pipeline id Job name should be ADF_factoryName_pipelineName Task name should be ADF_factoryName_pipelineName_activityName ADF Regex pattern should not ignore case --- .../parser/DatabricksToPurviewParser.cs | 53 +++++++++++++++++-- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/DatabricksToPurviewParser.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/DatabricksToPurviewParser.cs index d3e8c2b..d8c820b 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/DatabricksToPurviewParser.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/DatabricksToPurviewParser.cs @@ -14,6 +14,7 @@ using System.Security.Cryptography; using System.Text; using Newtonsoft.Json; +using System.Text.RegularExpressions; namespace Function.Domain.Helpers { @@ -30,6 +31,7 @@ public class DatabricksToPurviewParser: IDatabricksToPurviewParser private readonly EnrichedEvent _eEvent; private readonly string _adbWorkspaceUrl; const string SETTINGS = "OlToPurviewMappings"; + Regex ADF_JOB_NAME_REGEX = new Regex(@"^ADF_(.*)_(.*)_(.*)_(.*)$", RegexOptions.Compiled ); ///

/// Constructor for DatabricksToPurviewParser @@ -134,8 +136,18 @@ public DatabricksJob GetDatabricksJob(string workspaceQn) adbJobRoot = _eEvent.AdbRoot; } var databricksJob = new DatabricksJob(); - databricksJob.Attributes.Name = adbJobRoot.RunName; - databricksJob.Attributes.QualifiedName = $"databricks://{_adbWorkspaceUrl}.azuredatabricks.net/jobs/{_eEvent.AdbRoot!.JobId}"; + string _jobName = adbJobRoot.RunName; + string _jobId = _eEvent.AdbRoot!.JobId.ToString(); + // Special case for Azure Data Factory + // If we match this pattern in the job name, strip the last element since it's a random guid + // This will allow us to have the same name / qualified name each run + if (IsAdfJobName(_jobName)){ + _logger.LogInformation($"Azure Data Factory Job being processed: ({_jobName})"); + _jobName = TruncateAdfJobName(_jobName); + _jobId = _jobName; + } + databricksJob.Attributes.Name = _jobName; + databricksJob.Attributes.QualifiedName = $"databricks://{_adbWorkspaceUrl}.azuredatabricks.net/jobs/{_jobId}"; databricksJob.Attributes.JobId = adbJobRoot.JobId; databricksJob.Attributes.CreatorUserName = adbJobRoot.CreatorUserName; @@ -187,9 +199,20 @@ private void GetDatabricksJobTaskAttributes(DatabricksJobTaskAttributes taskAttr _logger.LogError(ex, ex.Message); throw ex; } - taskAttributes.Name = _eEvent.AdbRoot.JobTasks[0].TaskKey; - string jobQn = $"databricks://{_adbWorkspaceUrl}.azuredatabricks.net/jobs/{_eEvent.AdbRoot.JobId}"; - taskAttributes.QualifiedName = $"{jobQn}/tasks/{_eEvent.AdbRoot.JobTasks[0].TaskKey}"; + + string _taskKey = _eEvent.AdbRoot.JobTasks[0].TaskKey; + string _taskJobId = _eEvent.AdbRoot.JobId.ToString(); + // Special case for Azure Data Factory + // If we match this pattern in the job name, strip the last element since it's a random guid + // This will allow us to have the same name / qualified name each run + if (IsAdfJobName(_taskKey)){ + _logger.LogInformation($"Azure Data Factory Task being processed: ({_taskKey})"); + _taskJobId = TruncateAdfJobName(_taskKey); + _taskKey = TruncateAdfTaskName(_taskKey); + } + taskAttributes.Name = _taskKey; + string jobQn = $"databricks://{_adbWorkspaceUrl}.azuredatabricks.net/jobs/{_taskJobId}"; + taskAttributes.QualifiedName = $"{jobQn}/tasks/{_taskKey}"; taskAttributes.JobId = _eEvent.AdbRoot.JobId; taskAttributes.ClusterId = _eEvent.AdbRoot.JobTasks[0].ClusterInstance.ClusterId; taskAttributes.SparkVersion = _eEvent.OlEvent?.Run.Facets.SparkVersion.SparkVersion ?? ""; @@ -358,6 +381,26 @@ private string GenerateMd5Hash(string input) return sOutput.ToString(); } + // Special case for Azure Data Factory + // If we match this pattern in the job name, strip the last element since it's a random guid + // This will allow us to have the same name / qualified name each run + private bool IsAdfJobName(string inputName){ + // Follows the pattern ADF_factoryName_pipelineName_notebookName_pipelineRunId + return (ADF_JOB_NAME_REGEX.Matches(inputName).Count > 0); + } + private string TruncateAdfTaskName(string inputName){ + // Return ADF_factoryName_pipelineName_notebookName portions + string[] job_name_parts = inputName.Split("_"); + string[] job_name_except_last_element = job_name_parts.Take(job_name_parts.Count() - 1).ToArray(); + return string.Join("_", job_name_except_last_element); + } + private string TruncateAdfJobName(string inputName){ + // Return ADF_factoryName_pipelineName portions + string[] job_name_parts = inputName.Split("_"); + string[] job_name_except_last_element = job_name_parts.Take(job_name_parts.Count() - 2).ToArray(); + return string.Join("_", job_name_except_last_element); + } + public IColParser GetColumnParser() { return this._colParser; From 3fd940d49bf1e135ce0a99a5f0ae417c3deb9234 Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Thu, 23 Feb 2023 08:57:27 -0600 Subject: [PATCH 29/59] Reflect support for Azure Data Factory (#170) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4dc92e3..150f731 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ Gathering lineage data is performed in the following steps: * Azure Blob Storage * Delta Lake * Azure Data Explorer + * Azure Data Factory orchestration * MySQL * PostgreSQL * Supports Spark 3.0, 3.1, 3.2, and 3.3 (Interactive and Job clusters) / Spark 2.x (Job clusters) From f4b166b323d8f5690f935e7056ec931a88ba01a0 Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Thu, 23 Feb 2023 08:57:35 -0600 Subject: [PATCH 30/59] Update ADF and Kusto limitations (#169) --- LIMITATIONS.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/LIMITATIONS.md b/LIMITATIONS.md index d32b918..2ab0e35 100644 --- a/LIMITATIONS.md +++ b/LIMITATIONS.md @@ -91,7 +91,17 @@ Supports both Azure PostgreSQL and on-prem/VM installations of PostgreSQL throug ## Azure Data Explorer -Supports Azure Data Explorer (aka Kusto) through the [Azure Data Explorer Connector for Apache Spark](https://learn.microsoft.com/en-us/azure/data-explorer/spark-connector) +Supports Azure Data Explorer (aka Kusto) through the [Azure Data Explorer Connector for Apache Spark](https://learn.microsoft.com/en-us/azure/data-explorer/spark-connector). + +* Only supports the `kustoTable` option. +* If you use the `kustoQuery` option, it will return a Purview Generic Connector entity with a name of `COMPLEX` to capture the lineage but we are not able to parse arbitrary kusto queries at this time. + +## Azure Data Factory + +Supports capturing lineage for Databricks Notebook activities in Azure Data Factory (ADF). After running a notebook through ADF on an interactive or job cluster, you will see a Databricks Job asset in Microsoft Purview with a name similar to `ADF__`. For each Databricks notebook activity, you will also see a Databricks Task with a name similar to `ADF___`. + +* At this time, the Microsoft Purview view of Azure Data Factory lineage will not contain these tasks unless the Databricks Task uses or feeds a data source to a Data Flow or Copy activity. +* Copy Activities may not show lineage connecting to these Databricks tasks since it emits individual file assets rather than folder or resource set assets. ## Other Data Sources and Limitations @@ -109,10 +119,6 @@ Microsoft Purview's Fully Qualified Names are case sensitive. Spark Jobs may hav As a result, this solution attempts to find the best matching *existing* asset. If no existing asset is found to match based on qualified name, the data source name as found in the Spark query will be used toe create a dummy asset. On a subsequent scan of the data source in Purview and another run of the Spark query with the connector enabled will resolve the linkage. -### Data Factory - -The solution currently reflects the unfriendly job name provided by Data Factory to Databricks as noted in [issue 72](https://github.com/microsoft/Purview-ADB-Lineage-Solution-Accelerator/issues/72#issuecomment-1211202405). You will see jobs with names similar to `ADF____`. - ### Hive Metastore / Delta Table Names The solution currently does not support emitting the Hive Metastore / Delta table SQL names. For example, if you have a Delta table name `default.events` and it's physical location is `abfss://container@storage/path`, the solution will report `abfss://container@storage/path`. From 811a7ae8799fbc26ec4b14d8f0685edf0b73bb40 Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Thu, 23 Feb 2023 08:57:50 -0600 Subject: [PATCH 31/59] Update Delta Merge support (#167) Closes #156 --- LIMITATIONS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LIMITATIONS.md b/LIMITATIONS.md index 2ab0e35..b392cad 100644 --- a/LIMITATIONS.md +++ b/LIMITATIONS.md @@ -71,8 +71,8 @@ Supports Azure SQL DB through the [Apache Spark Connector for Azure SQL DB](http Supports [Delta File Format](https://delta.io/). +* Supports MERGE INTO statement on Databricks Runtime 10.4 LTS and higher. * Does not support Delta on Spark 2 Databricks Runtimes. -* Does not currently support the MERGE INTO statement due to differences between proprietary Databricks and Open Source Delta implementations. * Commands such as [Vacuum](https://docs.delta.io/latest/delta-utility.html#toc-entry-1) or [Optimize](https://docs.microsoft.com/en-us/azure/databricks/spark/latest/spark-sql/language-manual/delta-optimize) do not emit any lineage information and will not result in a Purview asset. ## Azure MySQL From 014183b7641f507fab1cdd338d654bf78d360739 Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Fri, 24 Feb 2023 11:16:55 -0600 Subject: [PATCH 32/59] Adding Hive Table as part of supported features (#171) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 150f731..a7c8535 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ Gathering lineage data is performed in the following steps: * Delta Lake * Azure Data Explorer * Azure Data Factory orchestration + * Hive Tables (in default metastore) * MySQL * PostgreSQL * Supports Spark 3.0, 3.1, 3.2, and 3.3 (Interactive and Job clusters) / Spark 2.x (Job clusters) From fbabc92d0a56a9025f2f866d8f16c43bac07fe54 Mon Sep 17 00:00:00 2001 From: Hanna Moazam Date: Fri, 24 Feb 2023 22:28:54 +0300 Subject: [PATCH 33/59] OL 13 -> 18 (#173) --- deployment/infra/openlineage-deployment.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/infra/openlineage-deployment.sh b/deployment/infra/openlineage-deployment.sh index 5af7d42..9d7a1d2 100644 --- a/deployment/infra/openlineage-deployment.sh +++ b/deployment/infra/openlineage-deployment.sh @@ -174,7 +174,7 @@ ADLSKEY=$(jq -r '.[1].value' <<< $adls_keys) CLUSTERNAME="openlineage-demo" ### Download Jar File -curl -O -L https://repo1.maven.org/maven2/io/openlineage/openlineage-spark/0.13.0/openlineage-spark-0.13.0.jar +curl -O -L https://repo1.maven.org/maven2/io/openlineage/openlineage-spark/0.18.0/openlineage-spark-0.18.0.jar ### az storage container create -n rawdata --account-name $ADLSNAME --account-key $ADLSKEY sampleA_resp=$(az storage blob upload --account-name $ADLSNAME --account-key $ADLSKEY -f exampleInputA.csv -c rawdata -n examples/data/csv/exampleInputA/exampleInputA.csv) From 5e657475e9adb90f93962843e8782476b4348c3c Mon Sep 17 00:00:00 2001 From: Hanna Moazam Date: Fri, 24 Feb 2023 22:29:36 +0300 Subject: [PATCH 34/59] Added snowflake mapping to gallery (#172) --- docs/mappings/README.md | 7 +++++++ docs/mappings/snowflake.json | 13 +++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 docs/mappings/snowflake.json diff --git a/docs/mappings/README.md b/docs/mappings/README.md index 0a3060e..f7909e3 100644 --- a/docs/mappings/README.md +++ b/docs/mappings/README.md @@ -14,3 +14,10 @@ This directory contains a "gallery" of sample OpenLineage to Purview Mappings th * [Prioritize Azure SQL Non DBO](./az-sql.json) * Default mappings treat an Azure SQL table named `myschema.mytable` as schema of `myschema` and table of `mytable`. * If you remove the `azureSQLNonDboNoDotsInNames` mapping, the above example would default to `dbo.[myschema.mytable]`. + +## Snowflake + +* [Snowflake](./snowflake.json) + * Supports mapping Snowflake tables in Purview. + * OpenLineage returns a DataSet with `"namespace":"snowflake://","name":"..` + * Microsoft Purview expects a fully qualified name of `snowflake:///databases//schemas//tables/
` \ No newline at end of file diff --git a/docs/mappings/snowflake.json b/docs/mappings/snowflake.json new file mode 100644 index 0000000..339ef54 --- /dev/null +++ b/docs/mappings/snowflake.json @@ -0,0 +1,13 @@ +{ + "name": "snowflake", + "parserConditions": [ + { + "op1": "prefix", + "compare": "=", + "op2": "snowflake" + } + ], + "qualifiedName": "snowflake://{nameSpcBodyParts[0]}/databases/{nameGroups[0].parts[0]}/schemas/{nameGroups[0].parts[1]}/tables/{nameGroups[0].parts[2]}", + "purviewDataType": "snowflake_table", + "purviewPrefix": "https" +} \ No newline at end of file From 39dcd362b8f3bee5f36d15236235c58856c95f75 Mon Sep 17 00:00:00 2001 From: Hanna Moazam Date: Fri, 24 Feb 2023 23:31:56 +0300 Subject: [PATCH 35/59] Updated with new aka.ms url ready for release (#168) --- deployment/infra/newdeploymenttemp.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/infra/newdeploymenttemp.json b/deployment/infra/newdeploymenttemp.json index 2674c85..5e426d4 100644 --- a/deployment/infra/newdeploymenttemp.json +++ b/deployment/infra/newdeploymenttemp.json @@ -151,7 +151,7 @@ "[concat('Microsoft.Web/sites/', variables('functionAppName'), '/config/web')]" ], "properties": { - "packageUri": "http://aka.ms/APFunctions2-2" + "packageUri": "http://aka.ms/APFunctions2-3" } }, { From 2c58203f4bc7b5953bbdbad24a55e9554acb5c7e Mon Sep 17 00:00:00 2001 From: Hanna Moazam Date: Sat, 25 Feb 2023 22:54:31 +0300 Subject: [PATCH 36/59] fixed MySQL and Postgres test expectations (#174) --- tests/integration/jobdefs/spark3-tests-expectations.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/jobdefs/spark3-tests-expectations.json b/tests/integration/jobdefs/spark3-tests-expectations.json index e413897..37814c8 100644 --- a/tests/integration/jobdefs/spark3-tests-expectations.json +++ b/tests/integration/jobdefs/spark3-tests-expectations.json @@ -42,10 +42,10 @@ "databricks://.azuredatabricks.net/notebooks/Shared/examples/synapse-wasbs-in-synapse-out", "databricks://.azuredatabricks.net/notebooks/Shared/examples/wasbs-in-wasbs-out", "databricks://.azuredatabricks.net/notebooks/Shared/examples/mysql-in-mysql-out", - "databricks://.azuredatabricks.net/notebooks/Shared/examples/mysql-in-mysql-out/processes/1F6965315A6049825A37C4AD085BD605->A08160B244AF828E1FDB80AC8D14FA96", + "databricks://.azuredatabricks.net/jobs//tasks/mysql-in-mysql-out/processes/1F6965315A6049825A37C4AD085BD605->A08160B244AF828E1FDB80AC8D14FA96", "databricks://.azuredatabricks.net/jobs//tasks/mysql-in-mysql-out", "databricks://.azuredatabricks.net/notebooks/Shared/examples/postgres-in-postgres-out", - "databricks://.azuredatabricks.net/notebooks/Shared/examples/postgres-in-postgres-out/processes/7E6CEF8EC093F119A11618169A8C4EAE->DB99105F739F05449E1ECD20A652DEE1", + "databricks://.azuredatabricks.net/jobs//tasks/postgres-in-postgres-out/processes/7E6CEF8EC093F119A11618169A8C4EAE->DB99105F739F05449E1ECD20A652DEE1", "databricks://.azuredatabricks.net/jobs//tasks/postgres-in-postgres-out", "databricks://.azuredatabricks.net/notebooks/Shared/examples/wasbs-in-kusto-out", "databricks://.azuredatabricks.net/jobs//tasks/wasbs-in-kusto-out", From b6b1e87aa18309811c560bb5dc8f518c70e2348b Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Tue, 28 Feb 2023 11:17:49 -0600 Subject: [PATCH 37/59] Fix Library Definitions in Job Tasks to prevent deserialization error from JSON to class models --- .../parser/DatabricksToPurviewParser.cs | 4 +- .../Models/Parser/Adb/JobTask.cs | 44 ++++++++++++++++++- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/DatabricksToPurviewParser.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/DatabricksToPurviewParser.cs index d8c820b..f7c7ca4 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/DatabricksToPurviewParser.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/DatabricksToPurviewParser.cs @@ -267,7 +267,7 @@ public DatabricksPythonWheelTask GetDatabricksPythonWheelTask(string jobQn) databricksPythonWheelTask.Attributes.PackageName = _eEvent.AdbRoot?.JobTasks?[0].PythonWheelTask?.PackageName ?? ""; databricksPythonWheelTask.Attributes.EntryPoint = _eEvent.AdbRoot?.JobTasks?[0].PythonWheelTask?.EntryPoint ?? ""; databricksPythonWheelTask.Attributes.Parameters = _eEvent.AdbRoot?.JobTasks?[0].PythonWheelTask?.Parameters ?? new List(); - databricksPythonWheelTask.Attributes.Wheel = _eEvent.AdbRoot?.JobTasks?[0].Libraries?[0]["whl"] ?? ""; + databricksPythonWheelTask.Attributes.Wheel = _eEvent.AdbRoot?.JobTasks?[0].Libraries?[0].wheelName ?? ""; databricksPythonWheelTask.RelationshipAttributes.Job.QualifiedName = jobQn; @@ -286,7 +286,7 @@ public DatabricksSparkJarTask GetDatabricksSparkJarTask(string jobQn) databricksSparkJarTask.Attributes.MainClassName = _eEvent.AdbRoot?.JobTasks?[0].SparkJarTask?.MainClassName ?? ""; databricksSparkJarTask.Attributes.JarUri = _eEvent.AdbRoot?.JobTasks?[0].SparkJarTask?.JarUri ?? ""; databricksSparkJarTask.Attributes.Parameters = _eEvent.AdbRoot?.JobTasks?[0].SparkJarTask?.Parameters ?? new List(); - databricksSparkJarTask.Attributes.Jar = _eEvent.AdbRoot?.JobTasks?[0].Libraries?[0]["jar"] ?? ""; + databricksSparkJarTask.Attributes.Jar = _eEvent.AdbRoot?.JobTasks?[0].Libraries?[0].jarName ?? ""; databricksSparkJarTask.RelationshipAttributes.Job.QualifiedName = jobQn; diff --git a/function-app/adb-to-purview/src/Function.Domain/Models/Parser/Adb/JobTask.cs b/function-app/adb-to-purview/src/Function.Domain/Models/Parser/Adb/JobTask.cs index 3d0ecb4..e3f5ddf 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Models/Parser/Adb/JobTask.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Models/Parser/Adb/JobTask.cs @@ -29,7 +29,7 @@ public class JobTask [JsonProperty("end_time")] public long EndTime = 0; [JsonProperty("libraries")] - public List>? Libraries = null; + public List? Libraries = null; [JsonProperty("notebook_task")] public NotebookTask? NotebookTask = null; [JsonProperty("spark_jar_task")] @@ -39,4 +39,44 @@ public class JobTask [JsonProperty("python_wheel_task")] public PythonWheelTask? PythonWheelTask = null; } -} \ No newline at end of file + public class JobLibrary + { + [JsonProperty("jar")] + public string? jarName = null; + [JsonProperty("egg")] + public string? eggName = null; + [JsonProperty("whl")] + public string? wheelName = null; + [JsonProperty("pypi")] + public PyPiJobLibrary? pypiLibrary = null; + [JsonProperty("maven")] + public MavenJobLibrary? mavenLibrary = null; + [JsonProperty("cran")] + public CranJobLibrary? cranLibrary = null; + } + public class PyPiJobLibrary + { + [JsonProperty("package")] + public string? package = null; + [JsonProperty("repo")] + public string? repo = null; + } + public class MavenJobLibrary + { + [JsonProperty("coordinates")] + public string? coordinates = null; + [JsonProperty("repo")] + public string? repo = null; + [JsonProperty("exclusions")] + public List? exclusions = null; + } + public class CranJobLibrary + { + [JsonProperty("package")] + public string? package = null; + [JsonProperty("repo")] + public string? repo = null; + } + +} + From bc40c97e135d6320b5ec22265fe1cf2a80aad760 Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Sun, 12 Mar 2023 21:03:00 -0600 Subject: [PATCH 38/59] OlToPurviewMapping Quality of Dev Improvements Python script to remove spaces and added as an artifact to Build and Release (Closes #183) Python script to update the arm template with the OlToPurviewMapping in a stringified json format (Closes #184) Updated the arm template with the standardized response from the new python script to avoid conflicts later on --- .github/workflows/build-release.yml | 11 + deployment/infra/newdeploymenttemp.json | 1037 +++++++++++---------- deployment/util/README.md | 36 + deployment/util/mappings-remove-spaces.py | 15 + deployment/util/mappings-update-arm.py | 34 + 5 files changed, 618 insertions(+), 515 deletions(-) create mode 100644 deployment/util/README.md create mode 100644 deployment/util/mappings-remove-spaces.py create mode 100644 deployment/util/mappings-update-arm.py diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 34b8b97..2a4e82e 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -69,6 +69,17 @@ jobs: name: FunctionZip path: ~/artifact/FunctionZip.zip + - name: Create One Line OlToPurviewMappings + run: | + mkdir ~/artifact-mappings + python ./deployment/util/mappings-remove-spaces.py ./deployment/infra/OlToPurviewMappings.json ~/artifact-mappings/one-line-mappings.json + + - name: Upload One Line OlToPurviewMappings Build Artifact + uses: actions/upload-artifact@v3 + with: + name: FunctionZip + path: ~/artifact-mappings/one-line-mappings.json + runIntegrationTests: name: Test on Integration Tests needs: [build] diff --git a/deployment/infra/newdeploymenttemp.json b/deployment/infra/newdeploymenttemp.json index 5e426d4..aad4fa2 100644 --- a/deployment/infra/newdeploymenttemp.json +++ b/deployment/infra/newdeploymenttemp.json @@ -1,515 +1,522 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "prefixName": { - "type": "string" - }, - "clientid": { - "type": "string" - }, - "clientsecret": { - "type": "securestring" - }, - "purviewName": { - "type": "string", - "defaultValue": "[concat(resourceGroup().name,'-openlineage-purview')]", - "metadata": { - "description": "User name for the Virtual Machine." - } - }, - "resourceTagValues": { - "type": "object" - }, - "functionSku": { - "type": "string", - "defaultValue": "Dynamic", - "metadata": { - "description": "The tier for the Azure Function." - }, - "allowedValues": [ - "Dynamic", - "EP1", - "EP2", - "EP3" - ] - } - }, - "variables": { - "paramName": "[parameters('prefixName')]", - "rgId": "[resourceGroup().id]", - "uniqueName": "[substring(uniqueString(variables('rgId')),0,4)]", - "functionAppName": "[replace(replace(toLower(concat(concat('functionapp',variables('paramName')),variables('uniqueName'))),'-',''),'_','')]", - "hostingPlanName": "[replace(replace(toLower(concat(concat('functionapphostplan',variables('paramName')),variables('uniqueName'))),'-',''),'_','')]", - "functionName": "OpenLineageIn", - "applicationInsightsName": "[replace(replace(toLower(concat(concat('appinsight',variables('paramName')),variables('uniqueName'))),'-',''),'_','')]", - "storageAccountName": "[replace(replace(toLower(concat(concat('storage',variables('paramName')),variables('uniqueName'))),'-',''),'_','')]", - "functionStorageAccountName": "[replace(replace(toLower(concat(concat('function','storage'),variables('uniqueName'))),'-',''),'_','')]", - "clientidkey": "clientIdKey", - "clientsecretkey": "clientSecretKey", - "storageAccountAccessKey": "storageAccessKey", - "functionStorageAccessKey": "functionStorageAccessKey", - "functionStorageAccountAccessKey": "functionStorageAccountKey", - "functionWorkerRuntime": "dotnet-isolated", - "openlineageEventHubNameSpaceName": "[replace(replace(toLower(concat(concat('eventhubns',variables('paramName')),variables('uniqueName'))),'-',''),'_','')]", - "openlineageNameEventHubName": "[replace(replace(toLower(concat(concat('eventhub',variables('paramName')),variables('uniqueName'))),'-',''),'_','')]", - "openlineageNameEventHubConsumerGroup": "[replace(replace(toLower(concat(concat('consumergroup',variables('paramName')),variables('uniqueName'))),'-',''),'_','')]", - "openlineageKeyVaultName": "[replace(replace(toLower(concat(concat('keyvaut',variables('paramName')),variables('uniqueName'))),'-',''),'_','')]", - "purviewAccountName": "[parameters('purviewName')]", - "eventHubSku": "Standard", - "captureEnabled": true, - "captureEncodingFormat": "Avro", - "captureTime": 60, - "captureSize": 314572800, - "EventHubConnectionSecretNameSend": "ehsecretSend", - "EventHubConnectionSecretNameListen": "ehsecretListen", - "functionStorageSecret": "functionStorageSecret", - "storageAccountSecret": "storageAccountSecret", - "OLOutputAPIKeySecretName": "Ol-Output-Api-Key", - "containerName": "eventhubdata", - "functionSkuDynamic":{"name": "Y1","tier": "Dynamic"}, - "functionSkuElasticPremium":{"tier": "ElasticPremium","name": "[parameters('functionSku')]","family": "EP"} - }, - "resources": [ - { - "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2019-06-01", - "name": "[variables('storageAccountName')]", - "location": "[resourceGroup().location]", - "sku": { - "name": "Standard_LRS" - }, - "kind": "Storage", - "tags": "[parameters('resourceTagValues')]", - "properties": { - "allowBlobPublicAccess": "False", - "supportsHttpsTrafficOnly": "True" - } - }, - { - "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2021-06-01", - "name": "[variables('functionStorageAccountName')]", - "location": "[resourceGroup().location]", - "sku": { - "name": "Standard_LRS" - }, - "kind": "Storage", - "tags": "[parameters('resourceTagValues')]", - "properties": { - "allowBlobPublicAccess": "False" - } - }, - { - "type": "Microsoft.Storage/storageAccounts/blobServices/containers", - "apiVersion": "2021-06-01", - "name": "[format('{0}/default/{1}', variables('functionStorageAccountName'), variables('containerName'))]", - "dependsOn": [ - "[resourceId('Microsoft.Storage/storageAccounts', variables('functionStorageAccountName'))]" - ] - }, - { - "type": "Microsoft.Web/serverfarms", - "apiVersion": "2020-06-01", - "name": "[variables('hostingPlanName')]", - "location": "[resourceGroup().location]", - "sku": "[if(equals(parameters('functionSku'), 'Dynamic'), variables('functionSkuDynamic'), variables('functionSkuElasticPremium'))]", - "tags": "[parameters('resourceTagValues')]", - "properties": { - "name": "[variables('hostingPlanName')]", - "computeMode": "Dynamic" - } - }, - { - "type": "Microsoft.Web/sites", - "apiVersion": "2020-06-01", - "name": "[variables('functionAppName')]", - "location": "[resourceGroup().location]", - "identity": { - "type": "SystemAssigned" - }, - "kind": "functionapp", - "tags": "[parameters('resourceTagValues')]", - "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", - "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", - "[resourceId('Microsoft.Insights/components', variables('applicationInsightsName'))]" - ], - "properties": { - "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", - "httpsOnly": true, - "siteConfig": {} - }, - "resources": [ - { - "name": "MSDeploy", - "type": "extensions", - "location": "[resourceGroup().location]", - "apiVersion": "2020-06-01", - "dependsOn": [ - "[concat('Microsoft.Web/sites/', variables('functionAppName'))]", - "[concat('Microsoft.Web/sites/', variables('functionAppName'), '/config/web')]" - ], - "properties": { - "packageUri": "http://aka.ms/APFunctions2-3" - } - }, - { - "apiVersion": "2020-06-01", - "type": "config", - "name": "web", - "dependsOn": [ - "[concat('Microsoft.Web/sites/', variables('functionAppName'))]" - ], - "properties": { - "appSettings": [ - { - "name": "AzureWebJobsStorage", - "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';EndpointSuffix=', environment().suffixes.storage, ';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value)]" - }, - { - "name": "FunctionStorage", - "value": "[concat('@Microsoft.KeyVault(VaultName=', variables('openlineageKeyVaultName'),';SecretName=',variables('functionStorageSecret'),')')]" - }, - { - "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", - "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';EndpointSuffix=', environment().suffixes.storage, ';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value)]" - }, - { - "name": "WEBSITE_CONTENTSHARE", - "value": "[toLower(variables('functionAppName'))]" - }, - { - "name": "FUNCTIONS_EXTENSION_VERSION", - "value": "~4" - }, - { - "name": "WEBSITE_NODE_DEFAULT_VERSION", - "value": "~10" - }, - { - "name": "APPINSIGHTS_INSTRUMENTATIONKEY", - "value": "[reference(resourceId('microsoft.insights/components', variables('applicationInsightsName')), '2020-02-02-preview').InstrumentationKey]" - }, - { - "name": "FUNCTIONS_WORKER_RUNTIME", - "value": "[variables('functionWorkerRuntime')]" - }, - { - "name": "EventHubName", - "value": "[variables('openlineageNameEventHubName')]" - }, - { - "name": "ListenToMessagesFromEventHub", - "value": "[concat('@Microsoft.KeyVault(VaultName=', variables('openlineageKeyVaultName'),';SecretName=',variables('EventHubConnectionSecretNameListen'),')')]" - }, - { - "name": "SendMessagesToEventHub", - "value": "[concat('@Microsoft.KeyVault(VaultName=', variables('openlineageKeyVaultName'),';SecretName=',variables('EventHubConnectionSecretNameSend'),')')]" - }, - { - "name": "EventHubConsumerGroup", - "value": "read" - }, - { - "name": "OlToPurviewMappings", - "value": "{\"olToPurviewMappings\":[{\"name\":\"wasbs\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"wasbs\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_blob_path\",\"purviewPrefix\":\"https\"},{\"name\":\"wasb\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"wasb\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_blob_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsBlobRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssBlobRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsBlob\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfs\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssBlob\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfss\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"synapseSqlNonDbo\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameSpcBodyParts[0]\",\"compare\":\"contains\",\"op2\":\"azuresynapse\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_synapse_dedicated_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"synapseSql\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameSpcBodyParts[0]\",\"compare\":\"contains\",\"op2\":\"azuresynapse\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/dbo/{nameGroups[0].parts[0]}\",\"purviewDataType\":\"azure_synapse_dedicated_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQLNonDbo\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameGroups\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0]}/{nameGroups[1]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQLNonDboNoDotsInNames\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQL\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/dbo/{nameGroups[0]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azurePostgresNonPublic\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"},{\"op1\":\"nameSpcConParts\",\"compare\":\">\",\"op2\":\"4\"},{\"op1\":\"nameSpcConParts[3]\",\"compare\":\"=\",\"op2\":\"azure\"}],\"qualifiedName\":\"postgresql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"azurePostgres\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameSpcConParts\",\"compare\":\">\",\"op2\":\"4\"},{\"op1\":\"nameSpcConParts[3]\",\"compare\":\"=\",\"op2\":\"azure\"}],\"qualifiedName\":\"postgresql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/public/{nameGroups[0]}\",\"purviewDataType\":\"azure_postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"postgresNonPublic\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"postgresql://servers/{nameSpcBodyParts[0]}:{nameSpcBodyParts[1]}/dbs/{nameSpcBodyParts[2]}/schemas/{nameGroups[0].parts[0]}/tables/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"postgres\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"}],\"qualifiedName\":\"postgresql://servers/{nameSpcBodyParts[0]}:{nameSpcBodyParts[1]}/dbs/{nameSpcBodyParts[2]}/schemas/public/tables/{nameGroups[0]}\",\"purviewDataType\":\"postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"hiveManagedTableNotDefault\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"dbfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"contains\",\"op2\":\"hive/warehouse\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"4\"}],\"qualifiedName\":\"{nameGroups[0].parts[3]}.{nameGroups[0].parts[5]}@{AdbWorkspaceUrl}\",\"purviewDataType\":\"hive_table\",\"purviewPrefix\":\"hive\"},{\"name\":\"hiveManagedTableDefault\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"dbfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"contains\",\"op2\":\"hive/warehouse\"}],\"qualifiedName\":\"default.{nameGroups[0].parts[3]}@{AdbWorkspaceUrl}\",\"purviewDataType\":\"hive_table\",\"purviewPrefix\":\"hive\"},{\"name\":\"azureMySql\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"mysql\"}],\"qualifiedName\":\"mysql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_mysql_table\",\"purviewPrefix\":\"mysql\"},{\"name\":\"kusto\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"azurekusto\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[0]}/{nameSpcBodyParts[1]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_data_explorer_table\",\"purviewPrefix\":\"https\"}]}" - }, - { - "name": "PurviewAccountName", - "value": "[variables('purviewAccountName')]" - }, - { - "name": "ClientID", - "value": "[concat('@Microsoft.KeyVault(VaultName=', variables('openlineageKeyVaultName'),';SecretName=',variables('clientidkey'),')')]" - }, - { - "name": "ClientSecret", - "value": "[concat('@Microsoft.KeyVault(VaultName=', variables('openlineageKeyVaultName'),';SecretName=',variables('clientsecretkey'),')')]" - }, - { - "name": "TenantId", - "value": "[subscription().tenantId]" - } - ] - } - } - ] - }, - { - "type": "microsoft.insights/components", - "apiVersion": "2020-02-02-preview", - "name": "[variables('applicationInsightsName')]", - "location": "[resourceGroup().location]", - "tags": { - "[concat('hidden-link:', resourceId('Microsoft.Web/sites', variables('applicationInsightsName')))]": "Resource" - }, - "properties": { - "ApplicationId": "[variables('applicationInsightsName')]", - "Request_Source": "IbizaWebAppExtensionCreate" - } - }, - { - "type": "Microsoft.EventHub/namespaces", - "apiVersion": "2018-01-01-preview", - "name": "[variables('openlineageEventHubNameSpaceName')]", - "location": "[resourceGroup().location]", - "sku": { - "name": "[variables('eventHubSku')]", - "tier": "[variables('eventHubSku')]", - "capacity": 1 - }, - "tags": "[parameters('resourceTagValues')]", - "properties": { - "isAutoInflateEnabled": false, - "maximumThroughputUnits": 0 - } - }, - { - "type": "Microsoft.EventHub/namespaces/eventhubs", - "apiVersion": "2017-04-01", - "name": "[concat(variables('openlineageEventHubNameSpaceName'), '/', variables('openlineageNameEventHubName'))]", - "location": "[resourceGroup().location]", - "tags": "[parameters('resourceTagValues')]", - "dependsOn": [ - "[resourceId('Microsoft.EventHub/namespaces', variables('openlineageEventHubNameSpaceName'))]" - ], - "properties": { - "messageRetentionInDays": 1, - "partitionCount": 1, - "captureDescription": { - "enabled": "[variables('captureEnabled')]", - "skipEmptyArchives": false, - "encoding": "[variables('captureEncodingFormat')]", - "intervalInSeconds": "[variables('captureTime')]", - "sizeLimitInBytes": "[variables('captureSize')]", - "destination": { - "name": "EventHubArchive.AzureBlockBlob", - "properties": { - "archiveNameFormat": "{Namespace}/{EventHub}/{PartitionId}/{Year}/{Month}/{Day}/{Hour}/{Minute}/{Second}", - "blobContainer": "eventhubdata", - "storageAccountResourceId": "[resourceId('Microsoft.Storage/storageAccounts', variables('functionStorageAccountName'))]" - } - } - } - }, - "resources": [ - { - "apiVersion": "2017-04-01", - "name": "read", - "type": "consumergroups", - "dependsOn": [ - "[variables('openlineageNameEventHubName')]" - ], - "properties": {} - } - ] - }, - { - "type": "Microsoft.KeyVault/vaults", - "name": "[variables('openlineageKeyVaultName')]", - "apiVersion": "2019-09-01", - "location": "[resourceGroup().location]", - "tags": "[parameters('resourceTagValues')]", - "properties": { - "sku": { - "family": "A", - "name": "Standard" - }, - "tenantId": "[subscription().tenantId]", - "accessPolicies": [ - { - "tenantId": "[subscription().tenantid]", - "objectId": "[reference(resourceId('Microsoft.Web/sites', variables('functionAppName')),'2020-06-01', 'full').identity.principalId]", - "permissions": { - "keys": [], - "secrets": [ - "get" - ], - "certificates": [] - } - } - ], - "enableSoftDelete": false, - "enabledForDeployment": false, - "enabledForDiskEncryption": false, - "enabledForTemplateDeployment": false - }, - "dependsOn": [ - "[resourceId('Microsoft.Web/sites', variables('functionAppName'))]" - ] - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2019-09-01", - "name": "[format('{0}/{1}', variables('openlineageKeyVaultName'), variables('EventHubConnectionSecretNameSend'))]", - "tags": "[parameters('resourceTagValues')]", - "properties": { - "value": "[listkeys(resourceId('Microsoft.Eventhub/namespaces/authorizationRules',variables('openlineageEventHubNameSpaceName'), 'SendMessages'),'2017-04-01').primaryConnectionString]" - }, - "dependsOn": [ - "[resourceId('Microsoft.EventHub/namespaces', variables('openlineageEventHubNameSpaceName'))]", - "[resourceId('Microsoft.KeyVault/vaults', variables('openlineageKeyVaultName'))]" - ] - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2019-09-01", - "name": "[format('{0}/{1}', variables('openlineageKeyVaultName'), variables('EventHubConnectionSecretNameListen'))]", - "tags": "[parameters('resourceTagValues')]", - "properties": { - "value": "[listkeys(resourceId('Microsoft.Eventhub/namespaces/authorizationRules',variables('openlineageEventHubNameSpaceName'), 'ListenMessages'),'2017-04-01').primaryConnectionString]" - }, - "dependsOn": [ - "[resourceId('Microsoft.EventHub/namespaces', variables('openlineageEventHubNameSpaceName'))]", - "[resourceId('Microsoft.KeyVault/vaults', variables('openlineageKeyVaultName'))]" - ] - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2019-09-01", - "name": "[format('{0}/{1}', variables('openlineageKeyVaultName'),variables('storageAccountName'))]", - "tags": "[parameters('resourceTagValues')]", - "properties": { - "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value,';EndpointSuffix=','core.windows.net')]" - }, - "dependsOn": [ - "[concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]", - "[resourceId('Microsoft.KeyVault/vaults', variables('openlineageKeyVaultName'))]" - ] - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2019-09-01", - "name": "[format('{0}/{1}', variables('openlineageKeyVaultName'),variables('functionStorageSecret'))]", - "tags": "[parameters('resourceTagValues')]", - "properties": { - "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('functionStorageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts',variables('functionStorageAccountName')),'2019-06-01').keys[0].value,';EndpointSuffix=','core.windows.net')]" - }, - "dependsOn": [ - "[concat('Microsoft.Storage/storageAccounts/', variables('functionStorageAccountName'))]", - "[resourceId('Microsoft.KeyVault/vaults', variables('openlineageKeyVaultName'))]" - ] - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2019-09-01", - "name": "[format('{0}/{1}', variables('openlineageKeyVaultName'),variables('OLOutputAPIKeySecretName'))]", - "tags": "[parameters('resourceTagValues')]", - "properties": { - "value": "[listKeys(concat(resourceId('Microsoft.Web/sites', variables('functionAppName')), '/host/default'), '2016-08-01').functionKeys.default]" - }, - "dependsOn": [ - "[resourceId('Microsoft.KeyVault/vaults', variables('openlineageKeyVaultName'))]" - ] - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2019-09-01", - "name": "[format('{0}/{1}', variables('openlineageKeyVaultName'),variables('storageAccountAccessKey'))]", - "tags": "[parameters('resourceTagValues')]", - "properties": { - "value": "[listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-04-01').keys[0].value]" - }, - "dependsOn": [ - "[resourceId('Microsoft.KeyVault/vaults', variables('openlineageKeyVaultName'))]" - ] - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2019-09-01", - "name": "[format('{0}/{1}', variables('openlineageKeyVaultName'),variables('functionStorageAccessKey'))]", - "tags": "[parameters('resourceTagValues')]", - "properties": { - "value": "[listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('functionStorageAccountName')), '2019-04-01').keys[0].value]" - }, - "dependsOn": [ - "[resourceId('Microsoft.KeyVault/vaults', variables('openlineageKeyVaultName'))]" - ] - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2019-09-01", - "name": "[format('{0}/{1}', variables('openlineageKeyVaultName'),variables('clientidkey'))]", - "tags": "[parameters('resourceTagValues')]", - "properties": { - "value": "[parameters('clientid')]" - }, - "dependsOn": [ - "[resourceId('Microsoft.KeyVault/vaults', variables('openlineageKeyVaultName'))]" - ] - }, - { - "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2019-09-01", - "name": "[format('{0}/{1}', variables('openlineageKeyVaultName'),variables('clientsecretkey'))]", - "tags": "[parameters('resourceTagValues')]", - "properties": { - "value": "[parameters('clientsecret')]" - }, - "dependsOn": [ - "[resourceId('Microsoft.KeyVault/vaults', variables('openlineageKeyVaultName'))]" - ] - }, - { - "type": "Microsoft.EventHub/namespaces/AuthorizationRules", - "apiVersion": "2021-11-01", - "name": "[concat(variables('openlineageEventHubNameSpaceName'), '/ListenMessages')]", - "tags": "[parameters('resourceTagValues')]", - "dependsOn": [ - "[resourceId('Microsoft.EventHub/namespaces', variables('openlineageEventHubNameSpaceName'))]", - "[resourceId('Microsoft.EventHub/namespaces/AuthorizationRules', variables('openlineageEventHubNameSpaceName'), 'SendMessages')]" - ], - "properties": { - "rights": [ - "Listen" - ] - } - }, - { - "type": "Microsoft.EventHub/namespaces/AuthorizationRules", - "apiVersion": "2021-11-01", - "name": "[concat(variables('openlineageEventHubNameSpaceName'), '/SendMessages')]", - "tags": "[parameters('resourceTagValues')]", - "dependsOn": [ - "[resourceId('Microsoft.EventHub/namespaces', variables('openlineageEventHubNameSpaceName'))]" - ], - "properties": { - "rights": [ - "Send" - ] - } - }, - { - "apiVersion": "2020-06-01", - "name": "pid-1e23d6fb-478f-4b04-bfa3-70db11929652", - "type": "Microsoft.Resources/deployments", - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "resources": [] - } - } - } - ], - "outputs": { - "functionAppName": { - "type": "string", - "value": "[variables('functionAppName')]" - }, - "kvName": { - "type": "string", - "value": "[variables('openlineageKeyVaultName')]" - }, - "storageAccountName": { - "type": "string", - "value": "[variables('storageAccountName')]" - }, - "resourcegroupLocation": { - "type": "string", - "value": "[resourceGroup().location]" - } - } -} +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "prefixName": { + "type": "string" + }, + "clientid": { + "type": "string" + }, + "clientsecret": { + "type": "securestring" + }, + "purviewName": { + "type": "string", + "defaultValue": "[concat(resourceGroup().name,'-openlineage-purview')]", + "metadata": { + "description": "User name for the Virtual Machine." + } + }, + "resourceTagValues": { + "type": "object" + }, + "functionSku": { + "type": "string", + "defaultValue": "Dynamic", + "metadata": { + "description": "The tier for the Azure Function." + }, + "allowedValues": [ + "Dynamic", + "EP1", + "EP2", + "EP3" + ] + } + }, + "variables": { + "paramName": "[parameters('prefixName')]", + "rgId": "[resourceGroup().id]", + "uniqueName": "[substring(uniqueString(variables('rgId')),0,4)]", + "functionAppName": "[replace(replace(toLower(concat(concat('functionapp',variables('paramName')),variables('uniqueName'))),'-',''),'_','')]", + "hostingPlanName": "[replace(replace(toLower(concat(concat('functionapphostplan',variables('paramName')),variables('uniqueName'))),'-',''),'_','')]", + "functionName": "OpenLineageIn", + "applicationInsightsName": "[replace(replace(toLower(concat(concat('appinsight',variables('paramName')),variables('uniqueName'))),'-',''),'_','')]", + "storageAccountName": "[replace(replace(toLower(concat(concat('storage',variables('paramName')),variables('uniqueName'))),'-',''),'_','')]", + "functionStorageAccountName": "[replace(replace(toLower(concat(concat('function','storage'),variables('uniqueName'))),'-',''),'_','')]", + "clientidkey": "clientIdKey", + "clientsecretkey": "clientSecretKey", + "storageAccountAccessKey": "storageAccessKey", + "functionStorageAccessKey": "functionStorageAccessKey", + "functionStorageAccountAccessKey": "functionStorageAccountKey", + "functionWorkerRuntime": "dotnet-isolated", + "openlineageEventHubNameSpaceName": "[replace(replace(toLower(concat(concat('eventhubns',variables('paramName')),variables('uniqueName'))),'-',''),'_','')]", + "openlineageNameEventHubName": "[replace(replace(toLower(concat(concat('eventhub',variables('paramName')),variables('uniqueName'))),'-',''),'_','')]", + "openlineageNameEventHubConsumerGroup": "[replace(replace(toLower(concat(concat('consumergroup',variables('paramName')),variables('uniqueName'))),'-',''),'_','')]", + "openlineageKeyVaultName": "[replace(replace(toLower(concat(concat('keyvaut',variables('paramName')),variables('uniqueName'))),'-',''),'_','')]", + "purviewAccountName": "[parameters('purviewName')]", + "eventHubSku": "Standard", + "captureEnabled": true, + "captureEncodingFormat": "Avro", + "captureTime": 60, + "captureSize": 314572800, + "EventHubConnectionSecretNameSend": "ehsecretSend", + "EventHubConnectionSecretNameListen": "ehsecretListen", + "functionStorageSecret": "functionStorageSecret", + "storageAccountSecret": "storageAccountSecret", + "OLOutputAPIKeySecretName": "Ol-Output-Api-Key", + "containerName": "eventhubdata", + "functionSkuDynamic": { + "name": "Y1", + "tier": "Dynamic" + }, + "functionSkuElasticPremium": { + "tier": "ElasticPremium", + "name": "[parameters('functionSku')]", + "family": "EP" + } + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2019-06-01", + "name": "[variables('storageAccountName')]", + "location": "[resourceGroup().location]", + "sku": { + "name": "Standard_LRS" + }, + "kind": "Storage", + "tags": "[parameters('resourceTagValues')]", + "properties": { + "allowBlobPublicAccess": "False", + "supportsHttpsTrafficOnly": "True" + } + }, + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2021-06-01", + "name": "[variables('functionStorageAccountName')]", + "location": "[resourceGroup().location]", + "sku": { + "name": "Standard_LRS" + }, + "kind": "Storage", + "tags": "[parameters('resourceTagValues')]", + "properties": { + "allowBlobPublicAccess": "False" + } + }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices/containers", + "apiVersion": "2021-06-01", + "name": "[format('{0}/default/{1}', variables('functionStorageAccountName'), variables('containerName'))]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('functionStorageAccountName'))]" + ] + }, + { + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2020-06-01", + "name": "[variables('hostingPlanName')]", + "location": "[resourceGroup().location]", + "sku": "[if(equals(parameters('functionSku'), 'Dynamic'), variables('functionSkuDynamic'), variables('functionSkuElasticPremium'))]", + "tags": "[parameters('resourceTagValues')]", + "properties": { + "name": "[variables('hostingPlanName')]", + "computeMode": "Dynamic" + } + }, + { + "type": "Microsoft.Web/sites", + "apiVersion": "2020-06-01", + "name": "[variables('functionAppName')]", + "location": "[resourceGroup().location]", + "identity": { + "type": "SystemAssigned" + }, + "kind": "functionapp", + "tags": "[parameters('resourceTagValues')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", + "[resourceId('Microsoft.Insights/components', variables('applicationInsightsName'))]" + ], + "properties": { + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", + "httpsOnly": true, + "siteConfig": {} + }, + "resources": [ + { + "name": "MSDeploy", + "type": "extensions", + "location": "[resourceGroup().location]", + "apiVersion": "2020-06-01", + "dependsOn": [ + "[concat('Microsoft.Web/sites/', variables('functionAppName'))]", + "[concat('Microsoft.Web/sites/', variables('functionAppName'), '/config/web')]" + ], + "properties": { + "packageUri": "http://aka.ms/APFunctions2-3" + } + }, + { + "apiVersion": "2020-06-01", + "type": "config", + "name": "web", + "dependsOn": [ + "[concat('Microsoft.Web/sites/', variables('functionAppName'))]" + ], + "properties": { + "appSettings": [ + { + "name": "AzureWebJobsStorage", + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';EndpointSuffix=', environment().suffixes.storage, ';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value)]" + }, + { + "name": "FunctionStorage", + "value": "[concat('@Microsoft.KeyVault(VaultName=', variables('openlineageKeyVaultName'),';SecretName=',variables('functionStorageSecret'),')')]" + }, + { + "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';EndpointSuffix=', environment().suffixes.storage, ';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value)]" + }, + { + "name": "WEBSITE_CONTENTSHARE", + "value": "[toLower(variables('functionAppName'))]" + }, + { + "name": "FUNCTIONS_EXTENSION_VERSION", + "value": "~4" + }, + { + "name": "WEBSITE_NODE_DEFAULT_VERSION", + "value": "~10" + }, + { + "name": "APPINSIGHTS_INSTRUMENTATIONKEY", + "value": "[reference(resourceId('microsoft.insights/components', variables('applicationInsightsName')), '2020-02-02-preview').InstrumentationKey]" + }, + { + "name": "FUNCTIONS_WORKER_RUNTIME", + "value": "[variables('functionWorkerRuntime')]" + }, + { + "name": "EventHubName", + "value": "[variables('openlineageNameEventHubName')]" + }, + { + "name": "ListenToMessagesFromEventHub", + "value": "[concat('@Microsoft.KeyVault(VaultName=', variables('openlineageKeyVaultName'),';SecretName=',variables('EventHubConnectionSecretNameListen'),')')]" + }, + { + "name": "SendMessagesToEventHub", + "value": "[concat('@Microsoft.KeyVault(VaultName=', variables('openlineageKeyVaultName'),';SecretName=',variables('EventHubConnectionSecretNameSend'),')')]" + }, + { + "name": "EventHubConsumerGroup", + "value": "read" + }, + { + "name": "OlToPurviewMappings", + "value": "{\"olToPurviewMappings\":[{\"name\":\"wasbs\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"wasbs\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_blob_path\",\"purviewPrefix\":\"https\"},{\"name\":\"wasb\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"wasb\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_blob_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsBlobRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssBlobRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsBlob\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfs\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssBlob\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfss\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"synapseSqlNonDbo\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameSpcBodyParts[0]\",\"compare\":\"contains\",\"op2\":\"azuresynapse\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_synapse_dedicated_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"synapseSql\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameSpcBodyParts[0]\",\"compare\":\"contains\",\"op2\":\"azuresynapse\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/dbo/{nameGroups[0].parts[0]}\",\"purviewDataType\":\"azure_synapse_dedicated_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQLNonDbo\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameGroups\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0]}/{nameGroups[1]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQLNonDboNoDotsInNames\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQL\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/dbo/{nameGroups[0]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azurePostgresNonPublic\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"},{\"op1\":\"nameSpcConParts\",\"compare\":\">\",\"op2\":\"4\"},{\"op1\":\"nameSpcConParts[3]\",\"compare\":\"=\",\"op2\":\"azure\"}],\"qualifiedName\":\"postgresql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"azurePostgres\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameSpcConParts\",\"compare\":\">\",\"op2\":\"4\"},{\"op1\":\"nameSpcConParts[3]\",\"compare\":\"=\",\"op2\":\"azure\"}],\"qualifiedName\":\"postgresql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/public/{nameGroups[0]}\",\"purviewDataType\":\"azure_postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"postgresNonPublic\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"postgresql://servers/{nameSpcBodyParts[0]}:{nameSpcBodyParts[1]}/dbs/{nameSpcBodyParts[2]}/schemas/{nameGroups[0].parts[0]}/tables/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"postgres\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"}],\"qualifiedName\":\"postgresql://servers/{nameSpcBodyParts[0]}:{nameSpcBodyParts[1]}/dbs/{nameSpcBodyParts[2]}/schemas/public/tables/{nameGroups[0]}\",\"purviewDataType\":\"postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"hiveManagedTableNotDefault\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"dbfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"contains\",\"op2\":\"hive/warehouse\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"4\"}],\"qualifiedName\":\"{nameGroups[0].parts[3]}.{nameGroups[0].parts[5]}@{AdbWorkspaceUrl}\",\"purviewDataType\":\"hive_table\",\"purviewPrefix\":\"hive\"},{\"name\":\"hiveManagedTableDefault\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"dbfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"contains\",\"op2\":\"hive/warehouse\"}],\"qualifiedName\":\"default.{nameGroups[0].parts[3]}@{AdbWorkspaceUrl}\",\"purviewDataType\":\"hive_table\",\"purviewPrefix\":\"hive\"},{\"name\":\"azureMySql\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"mysql\"}],\"qualifiedName\":\"mysql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_mysql_table\",\"purviewPrefix\":\"mysql\"},{\"name\":\"kusto\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"azurekusto\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[0]}/{nameSpcBodyParts[1]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_data_explorer_table\",\"purviewPrefix\":\"https\"}]}" + }, + { + "name": "PurviewAccountName", + "value": "[variables('purviewAccountName')]" + }, + { + "name": "ClientID", + "value": "[concat('@Microsoft.KeyVault(VaultName=', variables('openlineageKeyVaultName'),';SecretName=',variables('clientidkey'),')')]" + }, + { + "name": "ClientSecret", + "value": "[concat('@Microsoft.KeyVault(VaultName=', variables('openlineageKeyVaultName'),';SecretName=',variables('clientsecretkey'),')')]" + }, + { + "name": "TenantId", + "value": "[subscription().tenantId]" + } + ] + } + } + ] + }, + { + "type": "microsoft.insights/components", + "apiVersion": "2020-02-02-preview", + "name": "[variables('applicationInsightsName')]", + "location": "[resourceGroup().location]", + "tags": { + "[concat('hidden-link:', resourceId('Microsoft.Web/sites', variables('applicationInsightsName')))]": "Resource" + }, + "properties": { + "ApplicationId": "[variables('applicationInsightsName')]", + "Request_Source": "IbizaWebAppExtensionCreate" + } + }, + { + "type": "Microsoft.EventHub/namespaces", + "apiVersion": "2018-01-01-preview", + "name": "[variables('openlineageEventHubNameSpaceName')]", + "location": "[resourceGroup().location]", + "sku": { + "name": "[variables('eventHubSku')]", + "tier": "[variables('eventHubSku')]", + "capacity": 1 + }, + "tags": "[parameters('resourceTagValues')]", + "properties": { + "isAutoInflateEnabled": false, + "maximumThroughputUnits": 0 + } + }, + { + "type": "Microsoft.EventHub/namespaces/eventhubs", + "apiVersion": "2017-04-01", + "name": "[concat(variables('openlineageEventHubNameSpaceName'), '/', variables('openlineageNameEventHubName'))]", + "location": "[resourceGroup().location]", + "tags": "[parameters('resourceTagValues')]", + "dependsOn": [ + "[resourceId('Microsoft.EventHub/namespaces', variables('openlineageEventHubNameSpaceName'))]" + ], + "properties": { + "messageRetentionInDays": 1, + "partitionCount": 1, + "captureDescription": { + "enabled": "[variables('captureEnabled')]", + "skipEmptyArchives": false, + "encoding": "[variables('captureEncodingFormat')]", + "intervalInSeconds": "[variables('captureTime')]", + "sizeLimitInBytes": "[variables('captureSize')]", + "destination": { + "name": "EventHubArchive.AzureBlockBlob", + "properties": { + "archiveNameFormat": "{Namespace}/{EventHub}/{PartitionId}/{Year}/{Month}/{Day}/{Hour}/{Minute}/{Second}", + "blobContainer": "eventhubdata", + "storageAccountResourceId": "[resourceId('Microsoft.Storage/storageAccounts', variables('functionStorageAccountName'))]" + } + } + } + }, + "resources": [ + { + "apiVersion": "2017-04-01", + "name": "read", + "type": "consumergroups", + "dependsOn": [ + "[variables('openlineageNameEventHubName')]" + ], + "properties": {} + } + ] + }, + { + "type": "Microsoft.KeyVault/vaults", + "name": "[variables('openlineageKeyVaultName')]", + "apiVersion": "2019-09-01", + "location": "[resourceGroup().location]", + "tags": "[parameters('resourceTagValues')]", + "properties": { + "sku": { + "family": "A", + "name": "Standard" + }, + "tenantId": "[subscription().tenantId]", + "accessPolicies": [ + { + "tenantId": "[subscription().tenantid]", + "objectId": "[reference(resourceId('Microsoft.Web/sites', variables('functionAppName')),'2020-06-01', 'full').identity.principalId]", + "permissions": { + "keys": [], + "secrets": [ + "get" + ], + "certificates": [] + } + } + ], + "enableSoftDelete": false, + "enabledForDeployment": false, + "enabledForDiskEncryption": false, + "enabledForTemplateDeployment": false + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('functionAppName'))]" + ] + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2019-09-01", + "name": "[format('{0}/{1}', variables('openlineageKeyVaultName'), variables('EventHubConnectionSecretNameSend'))]", + "tags": "[parameters('resourceTagValues')]", + "properties": { + "value": "[listkeys(resourceId('Microsoft.Eventhub/namespaces/authorizationRules',variables('openlineageEventHubNameSpaceName'), 'SendMessages'),'2017-04-01').primaryConnectionString]" + }, + "dependsOn": [ + "[resourceId('Microsoft.EventHub/namespaces', variables('openlineageEventHubNameSpaceName'))]", + "[resourceId('Microsoft.KeyVault/vaults', variables('openlineageKeyVaultName'))]" + ] + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2019-09-01", + "name": "[format('{0}/{1}', variables('openlineageKeyVaultName'), variables('EventHubConnectionSecretNameListen'))]", + "tags": "[parameters('resourceTagValues')]", + "properties": { + "value": "[listkeys(resourceId('Microsoft.Eventhub/namespaces/authorizationRules',variables('openlineageEventHubNameSpaceName'), 'ListenMessages'),'2017-04-01').primaryConnectionString]" + }, + "dependsOn": [ + "[resourceId('Microsoft.EventHub/namespaces', variables('openlineageEventHubNameSpaceName'))]", + "[resourceId('Microsoft.KeyVault/vaults', variables('openlineageKeyVaultName'))]" + ] + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2019-09-01", + "name": "[format('{0}/{1}', variables('openlineageKeyVaultName'),variables('storageAccountName'))]", + "tags": "[parameters('resourceTagValues')]", + "properties": { + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value,';EndpointSuffix=','core.windows.net')]" + }, + "dependsOn": [ + "[concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]", + "[resourceId('Microsoft.KeyVault/vaults', variables('openlineageKeyVaultName'))]" + ] + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2019-09-01", + "name": "[format('{0}/{1}', variables('openlineageKeyVaultName'),variables('functionStorageSecret'))]", + "tags": "[parameters('resourceTagValues')]", + "properties": { + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('functionStorageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts',variables('functionStorageAccountName')),'2019-06-01').keys[0].value,';EndpointSuffix=','core.windows.net')]" + }, + "dependsOn": [ + "[concat('Microsoft.Storage/storageAccounts/', variables('functionStorageAccountName'))]", + "[resourceId('Microsoft.KeyVault/vaults', variables('openlineageKeyVaultName'))]" + ] + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2019-09-01", + "name": "[format('{0}/{1}', variables('openlineageKeyVaultName'),variables('OLOutputAPIKeySecretName'))]", + "tags": "[parameters('resourceTagValues')]", + "properties": { + "value": "[listKeys(concat(resourceId('Microsoft.Web/sites', variables('functionAppName')), '/host/default'), '2016-08-01').functionKeys.default]" + }, + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', variables('openlineageKeyVaultName'))]" + ] + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2019-09-01", + "name": "[format('{0}/{1}', variables('openlineageKeyVaultName'),variables('storageAccountAccessKey'))]", + "tags": "[parameters('resourceTagValues')]", + "properties": { + "value": "[listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-04-01').keys[0].value]" + }, + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', variables('openlineageKeyVaultName'))]" + ] + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2019-09-01", + "name": "[format('{0}/{1}', variables('openlineageKeyVaultName'),variables('functionStorageAccessKey'))]", + "tags": "[parameters('resourceTagValues')]", + "properties": { + "value": "[listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('functionStorageAccountName')), '2019-04-01').keys[0].value]" + }, + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', variables('openlineageKeyVaultName'))]" + ] + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2019-09-01", + "name": "[format('{0}/{1}', variables('openlineageKeyVaultName'),variables('clientidkey'))]", + "tags": "[parameters('resourceTagValues')]", + "properties": { + "value": "[parameters('clientid')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', variables('openlineageKeyVaultName'))]" + ] + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2019-09-01", + "name": "[format('{0}/{1}', variables('openlineageKeyVaultName'),variables('clientsecretkey'))]", + "tags": "[parameters('resourceTagValues')]", + "properties": { + "value": "[parameters('clientsecret')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', variables('openlineageKeyVaultName'))]" + ] + }, + { + "type": "Microsoft.EventHub/namespaces/AuthorizationRules", + "apiVersion": "2021-11-01", + "name": "[concat(variables('openlineageEventHubNameSpaceName'), '/ListenMessages')]", + "tags": "[parameters('resourceTagValues')]", + "dependsOn": [ + "[resourceId('Microsoft.EventHub/namespaces', variables('openlineageEventHubNameSpaceName'))]", + "[resourceId('Microsoft.EventHub/namespaces/AuthorizationRules', variables('openlineageEventHubNameSpaceName'), 'SendMessages')]" + ], + "properties": { + "rights": [ + "Listen" + ] + } + }, + { + "type": "Microsoft.EventHub/namespaces/AuthorizationRules", + "apiVersion": "2021-11-01", + "name": "[concat(variables('openlineageEventHubNameSpaceName'), '/SendMessages')]", + "tags": "[parameters('resourceTagValues')]", + "dependsOn": [ + "[resourceId('Microsoft.EventHub/namespaces', variables('openlineageEventHubNameSpaceName'))]" + ], + "properties": { + "rights": [ + "Send" + ] + } + }, + { + "apiVersion": "2020-06-01", + "name": "pid-1e23d6fb-478f-4b04-bfa3-70db11929652", + "type": "Microsoft.Resources/deployments", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [] + } + } + } + ], + "outputs": { + "functionAppName": { + "type": "string", + "value": "[variables('functionAppName')]" + }, + "kvName": { + "type": "string", + "value": "[variables('openlineageKeyVaultName')]" + }, + "storageAccountName": { + "type": "string", + "value": "[variables('storageAccountName')]" + }, + "resourcegroupLocation": { + "type": "string", + "value": "[resourceGroup().location]" + } + } +} \ No newline at end of file diff --git a/deployment/util/README.md b/deployment/util/README.md new file mode 100644 index 0000000..3afad1c --- /dev/null +++ b/deployment/util/README.md @@ -0,0 +1,36 @@ +# Utilities for Deployment + +## mappings-remove-spaces + +Used in the Github Action for creating a deployment artifact that is easier to copy / paste or upload into an app setting for Azure Functions. + +``` +usage: mappings-remove-spaces.py [-h] mappings_json output_path + +positional arguments: + mappings_json File path of the mappings json + output_path File path where the oneline json file should land +``` + +Sample: +``` +python ./deployment/util/mappings-remove-spaces.py ./deployment/infra/OlToPurviewMappings.json ./test.json +``` + +## mappings-update-arm + +Used to update the ARM template in a standardized way + +``` +usage: mappings-update-arm.py [-h] mappings_json template_file output_path + +positional arguments: + mappings_json File path of the mappings json + template_file File path to the ARM template to be updated + output_path File path to the output +``` + +Sample: +``` +python ./deployment/util/mappings-update-arm.py ./deployment/infra/OlToPurviewMappings.json ./deployment/infra/newdeploymenttemp.json ./deployment/infra/newdeploymenttemp.json +``` diff --git a/deployment/util/mappings-remove-spaces.py b/deployment/util/mappings-remove-spaces.py new file mode 100644 index 0000000..4eafd8b --- /dev/null +++ b/deployment/util/mappings-remove-spaces.py @@ -0,0 +1,15 @@ +import argparse + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("mappings_json", help="File path of the mappings json") + parser.add_argument("output_path", help="File path where the oneline json file should land") + args, unknown_args = parser.parse_known_args() + + with open(args.mappings_json, 'r') as fp: + mappings = fp.read() + + oneliner = mappings.replace("\n", "").replace(" ", "") + with open(args.output_path, 'w') as output: + output.write(oneliner) diff --git a/deployment/util/mappings-update-arm.py b/deployment/util/mappings-update-arm.py new file mode 100644 index 0000000..ed21869 --- /dev/null +++ b/deployment/util/mappings-update-arm.py @@ -0,0 +1,34 @@ +import argparse +import json + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("mappings_json", help="File path of the mappings json") + parser.add_argument("template_file", help="File path to the ARM template to be updated") + parser.add_argument("output_path", help="File path to the output") + args, unknown_args = parser.parse_known_args() + + with open(args.mappings_json, 'r') as fp: + mappings = json.load(fp) + + with open(args.template_file, 'r') as arm_input: + arm = json.load(arm_input) + + for resource in arm["resources"]: + if resource["type"] != "Microsoft.Web/sites": + continue + + child_resources = resource["resources"] + for child_resource in child_resources: + if child_resource["type"] != "config": + continue + + for setting in child_resource["properties"]["appSettings"]: + if setting["name"] != "OlToPurviewMappings": + continue + setting["value"] = json.dumps(mappings).replace(" ", "") + print("Successfully updated mappings setting") + + + with open(args.output_path, 'w') as output: + json.dump(arm, output, indent="\t") From 46ae5b9f21400027b249b4bf5517414ce72ae046 Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Sun, 12 Mar 2023 21:21:00 -0600 Subject: [PATCH 39/59] Enabling Workflow Dispatch to run the build --- .github/workflows/build-release.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 2a4e82e..8a42a9c 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -9,6 +9,11 @@ on: - '**.csproj' - 'tests/integration/**' workflow_dispatch: + inputs: + tags: + description: 'Flag as workflow dispatch' + required: true + type: boolean env: DOTNET_VERSION: '6.x.x' # The .NET SDK version to use @@ -25,11 +30,13 @@ jobs: echo "Github Event Name: ${{ github.event_name }}" echo "Github Ref: ${{ github.ref }}" echo "Github Ref Type: ${{ github.ref_type }}" + echo "Github Tags: ${{ inputs.tags }}" build: if: | github.event_name == 'pull_request' || - (github.event_name == 'create' && github.ref_type == 'tag') + (github.event_name == 'create' && github.ref_type == 'tag') || + ${{github.event_name == 'create' && inputs.tags}} name: build-${{matrix.os}} runs-on: ${{ matrix.os }} strategy: From 05a54b28f179cd02c11236a75a36d81e3f5c618a Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Sun, 12 Mar 2023 22:51:16 -0500 Subject: [PATCH 40/59] Correct one line mappings as artifact --- .github/workflows/build-release.yml | 3 ++- deployment/util/README.md | 3 +-- deployment/util/mappings-remove-spaces.py | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 8a42a9c..c72af22 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -79,7 +79,8 @@ jobs: - name: Create One Line OlToPurviewMappings run: | mkdir ~/artifact-mappings - python ./deployment/util/mappings-remove-spaces.py ./deployment/infra/OlToPurviewMappings.json ~/artifact-mappings/one-line-mappings.json + python ./deployment/util/mappings-remove-spaces.py ./deployment/infra/OlToPurviewMappings.json > ~/artifact-mappings/one-line-mappings.json + ls ~/artifact-mappings - name: Upload One Line OlToPurviewMappings Build Artifact uses: actions/upload-artifact@v3 diff --git a/deployment/util/README.md b/deployment/util/README.md index 3afad1c..ed60061 100644 --- a/deployment/util/README.md +++ b/deployment/util/README.md @@ -9,12 +9,11 @@ usage: mappings-remove-spaces.py [-h] mappings_json output_path positional arguments: mappings_json File path of the mappings json - output_path File path where the oneline json file should land ``` Sample: ``` -python ./deployment/util/mappings-remove-spaces.py ./deployment/infra/OlToPurviewMappings.json ./test.json +python ./deployment/util/mappings-remove-spaces.py ./deployment/infra/OlToPurviewMappings.json > test.json ``` ## mappings-update-arm diff --git a/deployment/util/mappings-remove-spaces.py b/deployment/util/mappings-remove-spaces.py index 4eafd8b..f50989c 100644 --- a/deployment/util/mappings-remove-spaces.py +++ b/deployment/util/mappings-remove-spaces.py @@ -1,15 +1,14 @@ import argparse +import os if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("mappings_json", help="File path of the mappings json") - parser.add_argument("output_path", help="File path where the oneline json file should land") args, unknown_args = parser.parse_known_args() with open(args.mappings_json, 'r') as fp: mappings = fp.read() oneliner = mappings.replace("\n", "").replace(" ", "") - with open(args.output_path, 'w') as output: - output.write(oneliner) + print(oneliner) From 6fd492e078103262f926023c51cd9239f3d92cab Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Wed, 1 Mar 2023 01:30:29 -0600 Subject: [PATCH 41/59] Fix Mappings for Mount Points with Subdirectories Mount points that have a source with a subdirectory were failing to include the subdirectories Introduced a new namespaceBodyPartsJoinedBySlashFrom mappings variable to capture any parts from a certain position and concatenate them with a forward slash. --- deployment/infra/OlToPurviewMappings.json | 223 ++++++++++++++++++ deployment/infra/newdeploymenttemp.json | 2 +- docs/mappings/adlsg1.json | 18 ++ .../Helpers/parser/QnParser.cs | 2 +- .../Models/Parser/OpenLineage/OlParts.cs | 15 +- .../Helpers/Parser/QnParserTests.cs | 8 +- .../Helpers/Parser/UnitTestData.cs | 4 +- 7 files changed, 265 insertions(+), 7 deletions(-) diff --git a/deployment/infra/OlToPurviewMappings.json b/deployment/infra/OlToPurviewMappings.json index 7c68f45..b6918a2 100644 --- a/deployment/infra/OlToPurviewMappings.json +++ b/deployment/infra/OlToPurviewMappings.json @@ -1,5 +1,23 @@ { "olToPurviewMappings": [ + { + "name": "wasbsNLayerMnt", + "parserConditions": [ + { + "op1": "prefix", + "compare": "=", + "op2": "wasbs" + }, + { + "op1": "nameSpcBodyParts", + "compare": ">", + "op2": "2" + } + ], + "qualifiedName": "https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}", + "purviewDataType": "azure_blob_path", + "purviewPrefix": "https" + }, { "name": "wasbs", "parserConditions": [ @@ -13,6 +31,24 @@ "purviewDataType": "azure_blob_path", "purviewPrefix": "https" }, + { + "name": "wasbNLayerMnt", + "parserConditions": [ + { + "op1": "prefix", + "compare": "=", + "op2": "wasb" + }, + { + "op1": "nameSpcBodyParts", + "compare": ">", + "op2": "2" + } + ], + "qualifiedName": "https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}", + "purviewDataType": "azure_blob_path", + "purviewPrefix": "https" + }, { "name": "wasb", "parserConditions": [ @@ -26,6 +62,34 @@ "purviewDataType": "azure_blob_path", "purviewPrefix": "https" }, + { + "name": "abfsBlobRootFSNLayerMnt", + "parserConditions": [ + { + "op1": "prefix", + "compare": "=", + "op2": "abfs" + }, + { + "op1": "nameSpcBodyParts[1]", + "compare": "contains", + "op2": "blob" + }, + { + "op1": "nameGroups[0]", + "compare": "=", + "op2": "" + }, + { + "op1": "nameSpcBodyParts", + "compare": ">", + "op2": "2" + } + ], + "qualifiedName": "https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}", + "purviewDataType": "azure_datalake_gen2_filesystem", + "purviewPrefix": "https" + }, { "name": "abfsBlobRootFS", "parserConditions": [ @@ -49,6 +113,29 @@ "purviewDataType": "azure_datalake_gen2_filesystem", "purviewPrefix": "https" }, + { + "name": "abfsRootFSNLayerMnt", + "parserConditions": [ + { + "op1": "prefix", + "compare": "=", + "op2": "abfs" + }, + { + "op1": "nameGroups[0]", + "compare": "=", + "op2": "" + }, + { + "op1": "nameSpcBodyParts", + "compare": ">", + "op2": "2" + } + ], + "qualifiedName": "https://{nameSpcBodyParts[1]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}", + "purviewDataType": "azure_datalake_gen2_filesystem", + "purviewPrefix": "https" + }, { "name": "abfsRootFS", "parserConditions": [ @@ -67,6 +154,34 @@ "purviewDataType": "azure_datalake_gen2_filesystem", "purviewPrefix": "https" }, + { + "name": "abfssBlobRootFSNLayerMnt", + "parserConditions": [ + { + "op1": "prefix", + "compare": "=", + "op2": "abfss" + }, + { + "op1": "nameSpcBodyParts[1]", + "compare": "contains", + "op2": "blob" + }, + { + "op1": "nameGroups[0]", + "compare": "=", + "op2": "" + }, + { + "op1": "nameSpcBodyParts", + "compare": ">", + "op2": "2" + } + ], + "qualifiedName": "https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}", + "purviewDataType": "azure_datalake_gen2_filesystem", + "purviewPrefix": "https" + }, { "name": "abfssBlobRootFS", "parserConditions": [ @@ -90,6 +205,29 @@ "purviewDataType": "azure_datalake_gen2_filesystem", "purviewPrefix": "https" }, + { + "name": "abfssRootFSNLayerMnt", + "parserConditions": [ + { + "op1": "prefix", + "compare": "=", + "op2": "abfss" + }, + { + "op1": "nameGroups[0]", + "compare": "=", + "op2": "" + }, + { + "op1": "nameSpcBodyParts", + "compare": ">", + "op2": "2" + } + ], + "qualifiedName": "https://{nameSpcBodyParts[1]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}", + "purviewDataType": "azure_datalake_gen2_filesystem", + "purviewPrefix": "https" + }, { "name": "abfssRootFS", "parserConditions": [ @@ -108,6 +246,30 @@ "purviewDataType": "azure_datalake_gen2_filesystem", "purviewPrefix": "https" }, + { + "name": "abfsBlobNLayerMnt", + "parserConditions": [ + { + "op1": "prefix", + "compare": "=", + "op2": "abfs" + }, + { + "op1": "nameSpcBodyParts[1]", + "compare": "contains", + "op2": "blob" + }, + { + "op1": "nameSpcBodyParts", + "compare": ">", + "op2": "2" + } + + ], + "qualifiedName": "https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}", + "purviewDataType": "azure_datalake_gen2_path", + "purviewPrefix": "https" + }, { "name": "abfsBlob", "parserConditions": [ @@ -126,6 +288,25 @@ "purviewDataType": "azure_datalake_gen2_path", "purviewPrefix": "https" }, + { + "name": "abfsNLayerMnt", + "parserConditions": [ + { + "op1": "prefix", + "compare": "=", + "op2": "abfs" + }, + { + "op1": "nameSpcBodyParts", + "compare": ">", + "op2": "2" + } + + ], + "qualifiedName": "https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}", + "purviewDataType": "azure_datalake_gen2_path", + "purviewPrefix": "https" + }, { "name": "abfs", "parserConditions": [ @@ -139,6 +320,29 @@ "purviewDataType": "azure_datalake_gen2_path", "purviewPrefix": "https" }, + { + "name": "abfssBlobNLayerMnt", + "parserConditions": [ + { + "op1": "prefix", + "compare": "=", + "op2": "abfss" + }, + { + "op1": "nameSpcBodyParts[1]", + "compare": "contains", + "op2": "blob" + }, + { + "op1": "nameSpcBodyParts", + "compare": ">", + "op2": "2" + } + ], + "qualifiedName": "https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}", + "purviewDataType": "azure_datalake_gen2_path", + "purviewPrefix": "https" + }, { "name": "abfssBlob", "parserConditions": [ @@ -157,6 +361,25 @@ "purviewDataType": "azure_datalake_gen2_path", "purviewPrefix": "https" }, + { + "name": "abfssNLayerMnt", + "parserConditions": [ + { + "op1": "prefix", + "compare": "=", + "op2": "abfss" + }, + { + "op1": "nameSpcBodyParts", + "compare": ">", + "op2": "2" + } + + ], + "qualifiedName": "https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}", + "purviewDataType": "azure_datalake_gen2_path", + "purviewPrefix": "https" + }, { "name": "abfss", "parserConditions": [ diff --git a/deployment/infra/newdeploymenttemp.json b/deployment/infra/newdeploymenttemp.json index aad4fa2..d82a238 100644 --- a/deployment/infra/newdeploymenttemp.json +++ b/deployment/infra/newdeploymenttemp.json @@ -220,7 +220,7 @@ }, { "name": "OlToPurviewMappings", - "value": "{\"olToPurviewMappings\":[{\"name\":\"wasbs\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"wasbs\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_blob_path\",\"purviewPrefix\":\"https\"},{\"name\":\"wasb\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"wasb\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_blob_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsBlobRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssBlobRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsBlob\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfs\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssBlob\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfss\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"synapseSqlNonDbo\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameSpcBodyParts[0]\",\"compare\":\"contains\",\"op2\":\"azuresynapse\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_synapse_dedicated_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"synapseSql\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameSpcBodyParts[0]\",\"compare\":\"contains\",\"op2\":\"azuresynapse\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/dbo/{nameGroups[0].parts[0]}\",\"purviewDataType\":\"azure_synapse_dedicated_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQLNonDbo\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameGroups\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0]}/{nameGroups[1]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQLNonDboNoDotsInNames\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQL\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/dbo/{nameGroups[0]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azurePostgresNonPublic\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"},{\"op1\":\"nameSpcConParts\",\"compare\":\">\",\"op2\":\"4\"},{\"op1\":\"nameSpcConParts[3]\",\"compare\":\"=\",\"op2\":\"azure\"}],\"qualifiedName\":\"postgresql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"azurePostgres\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameSpcConParts\",\"compare\":\">\",\"op2\":\"4\"},{\"op1\":\"nameSpcConParts[3]\",\"compare\":\"=\",\"op2\":\"azure\"}],\"qualifiedName\":\"postgresql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/public/{nameGroups[0]}\",\"purviewDataType\":\"azure_postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"postgresNonPublic\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"postgresql://servers/{nameSpcBodyParts[0]}:{nameSpcBodyParts[1]}/dbs/{nameSpcBodyParts[2]}/schemas/{nameGroups[0].parts[0]}/tables/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"postgres\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"}],\"qualifiedName\":\"postgresql://servers/{nameSpcBodyParts[0]}:{nameSpcBodyParts[1]}/dbs/{nameSpcBodyParts[2]}/schemas/public/tables/{nameGroups[0]}\",\"purviewDataType\":\"postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"hiveManagedTableNotDefault\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"dbfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"contains\",\"op2\":\"hive/warehouse\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"4\"}],\"qualifiedName\":\"{nameGroups[0].parts[3]}.{nameGroups[0].parts[5]}@{AdbWorkspaceUrl}\",\"purviewDataType\":\"hive_table\",\"purviewPrefix\":\"hive\"},{\"name\":\"hiveManagedTableDefault\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"dbfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"contains\",\"op2\":\"hive/warehouse\"}],\"qualifiedName\":\"default.{nameGroups[0].parts[3]}@{AdbWorkspaceUrl}\",\"purviewDataType\":\"hive_table\",\"purviewPrefix\":\"hive\"},{\"name\":\"azureMySql\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"mysql\"}],\"qualifiedName\":\"mysql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_mysql_table\",\"purviewPrefix\":\"mysql\"},{\"name\":\"kusto\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"azurekusto\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[0]}/{nameSpcBodyParts[1]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_data_explorer_table\",\"purviewPrefix\":\"https\"}]}" + "value": "{\"olToPurviewMappings\":[{\"name\":\"wasbsNLayerMnt\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"wasbs\"},{\"op1\":\"nameSpcBodyParts\",\"compare\":\">\",\"op2\":\"2\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_blob_path\",\"purviewPrefix\":\"https\"},{\"name\":\"wasbs\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"wasbs\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_blob_path\",\"purviewPrefix\":\"https\"},{\"name\":\"wasbNLayerMnt\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"wasb\"},{\"op1\":\"nameSpcBodyParts\",\"compare\":\">\",\"op2\":\"2\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_blob_path\",\"purviewPrefix\":\"https\"},{\"name\":\"wasb\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"wasb\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_blob_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsBlobRootFSNLayerMnt\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"},{\"op1\":\"nameSpcBodyParts\",\"compare\":\">\",\"op2\":\"2\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsBlobRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsRootFSNLayerMnt\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"},{\"op1\":\"nameSpcBodyParts\",\"compare\":\">\",\"op2\":\"2\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssBlobRootFSNLayerMnt\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"},{\"op1\":\"nameSpcBodyParts\",\"compare\":\">\",\"op2\":\"2\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssBlobRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssRootFSNLayerMnt\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"},{\"op1\":\"nameSpcBodyParts\",\"compare\":\">\",\"op2\":\"2\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsBlobNLayerMnt\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameSpcBodyParts\",\"compare\":\">\",\"op2\":\"2\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsBlob\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsNLayerMnt\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameSpcBodyParts\",\"compare\":\">\",\"op2\":\"2\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfs\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssBlobNLayerMnt\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameSpcBodyParts\",\"compare\":\">\",\"op2\":\"2\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssBlob\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssNLayerMnt\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameSpcBodyParts\",\"compare\":\">\",\"op2\":\"2\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfss\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"synapseSqlNonDbo\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameSpcBodyParts[0]\",\"compare\":\"contains\",\"op2\":\"azuresynapse\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_synapse_dedicated_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"synapseSql\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameSpcBodyParts[0]\",\"compare\":\"contains\",\"op2\":\"azuresynapse\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/dbo/{nameGroups[0].parts[0]}\",\"purviewDataType\":\"azure_synapse_dedicated_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQLNonDbo\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameGroups\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0]}/{nameGroups[1]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQLNonDboNoDotsInNames\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQL\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/dbo/{nameGroups[0]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azurePostgresNonPublic\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"},{\"op1\":\"nameSpcConParts\",\"compare\":\">\",\"op2\":\"4\"},{\"op1\":\"nameSpcConParts[3]\",\"compare\":\"=\",\"op2\":\"azure\"}],\"qualifiedName\":\"postgresql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"azurePostgres\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameSpcConParts\",\"compare\":\">\",\"op2\":\"4\"},{\"op1\":\"nameSpcConParts[3]\",\"compare\":\"=\",\"op2\":\"azure\"}],\"qualifiedName\":\"postgresql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/public/{nameGroups[0]}\",\"purviewDataType\":\"azure_postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"postgresNonPublic\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"postgresql://servers/{nameSpcBodyParts[0]}:{nameSpcBodyParts[1]}/dbs/{nameSpcBodyParts[2]}/schemas/{nameGroups[0].parts[0]}/tables/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"postgres\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"}],\"qualifiedName\":\"postgresql://servers/{nameSpcBodyParts[0]}:{nameSpcBodyParts[1]}/dbs/{nameSpcBodyParts[2]}/schemas/public/tables/{nameGroups[0]}\",\"purviewDataType\":\"postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"hiveManagedTableNotDefault\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"dbfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"contains\",\"op2\":\"hive/warehouse\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"4\"}],\"qualifiedName\":\"{nameGroups[0].parts[3]}.{nameGroups[0].parts[5]}@{AdbWorkspaceUrl}\",\"purviewDataType\":\"hive_table\",\"purviewPrefix\":\"hive\"},{\"name\":\"hiveManagedTableDefault\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"dbfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"contains\",\"op2\":\"hive/warehouse\"}],\"qualifiedName\":\"default.{nameGroups[0].parts[3]}@{AdbWorkspaceUrl}\",\"purviewDataType\":\"hive_table\",\"purviewPrefix\":\"hive\"},{\"name\":\"azureMySql\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"mysql\"}],\"qualifiedName\":\"mysql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_mysql_table\",\"purviewPrefix\":\"mysql\"},{\"name\":\"kusto\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"azurekusto\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[0]}/{nameSpcBodyParts[1]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_data_explorer_table\",\"purviewPrefix\":\"https\"}]}" }, { "name": "PurviewAccountName", diff --git a/docs/mappings/adlsg1.json b/docs/mappings/adlsg1.json index 402af39..ace5c93 100644 --- a/docs/mappings/adlsg1.json +++ b/docs/mappings/adlsg1.json @@ -1,3 +1,21 @@ +{ + "name": "adlsg1", + "parserConditions": [ + { + "op1": "prefix", + "compare": "=", + "op2": "adl" + }, + { + "op1": "nameSpcBodyParts", + "compare": ">", + "op2": "1" + } + ], + "qualifiedName": "adl://{nameSpcBodyParts[0]}/{nameSpaceBodyJoinedBySlashFrom[1]}/{nameGroups[0]}", + "purviewDataType": "azure_datalake_gen1_path", + "purviewPrefix": "adl" +}, { "name": "adlsg1", "parserConditions": [ diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/QnParser.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/QnParser.cs index b02ca72..9c403e6 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/QnParser.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/parser/QnParser.cs @@ -22,7 +22,7 @@ public class QnParser: IQnParser private ILogger _logger; private readonly string[] JSON_KEY_NAMES = { "prefix", "nameSpcConParts", "nameSpcBodyParts", "nameSpcNameVals", - "nameGroups"}; + "nameGroups", "nameSpaceBodyJoinedBySlashFrom"}; /// /// Constructor for QnParser diff --git a/function-app/adb-to-purview/src/Function.Domain/Models/Parser/OpenLineage/OlParts.cs b/function-app/adb-to-purview/src/Function.Domain/Models/Parser/OpenLineage/OlParts.cs index 7a4208c..76502e2 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Models/Parser/OpenLineage/OlParts.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Models/Parser/OpenLineage/OlParts.cs @@ -43,6 +43,14 @@ public OlParts(string nameSpace, string name) } } } + // Add Support for NameSpaceBodyJoinedBySlash to enable mount points with trailing folders + for (int nmSpPos = this._olNameSpaceParts.NameSpaceBodyParts.Count(); nmSpPos > 0; nmSpPos--) + { + this._olNameSpaceParts.NameSpaceBodyJoinedBySlashFrom.Add( + String.Join('/', this._olNameSpaceParts.NameSpaceBodyParts.TakeLast(nmSpPos)) + ); + + } var rgex = new Regex(@"(?<=\[).+?(?=\])"); var rerslt = rgex.Matches(name); if (rerslt.Count > 0) @@ -100,9 +108,9 @@ private bool AddConStringNameValues(string inString, ref Dictionary public Dictionary GetDynamicPairs(string[] keys) { - if (keys == null || keys.Length != 5) + if (keys == null || keys.Length != 6) { - throw new System.ArgumentException("keys must be an array of length 5"); + throw new System.ArgumentException("keys must be an array of length 6"); } var pairs = new Dictionary(); pairs.Add(keys[0], this.Prefix); @@ -110,6 +118,7 @@ public Dictionary GetDynamicPairs(string[] keys) pairs.Add(keys[2], this.OlNameSpaceParts.NameSpaceBodyParts); pairs.Add(keys[3], this.OlNameSpaceParts.NameSpaceConnNameValues); pairs.Add(keys[4], this.OlNameParts.NameGroups); + pairs.Add(keys[5], this.OlNameSpaceParts.NameSpaceBodyJoinedBySlashFrom); return pairs; } @@ -126,6 +135,8 @@ public class OlNameSpaceParts public List NameSpaceConnParts = new List(); // Splits out any name value pairs as identified by = symbol public Dictionary NameSpaceConnNameValues = new Dictionary(); + // Joins the NameSpaceBodyParts joined by a forward slash + public List NameSpaceBodyJoinedBySlashFrom = new List(); } public class OlNameParts diff --git a/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs b/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs index 42f31d2..5ae75a2 100644 --- a/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs +++ b/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs @@ -91,8 +91,12 @@ public QnParserTests() // DBFS mount trailing slash in def [InlineData("dbfs", "/mnt/purview2", - "https://purviewexamplessa.dfs.core.windows.net/purview2")] - // Azure SQL Non DBO Schema - + "https://purviewexamplessa.dfs.core.windows.net/purview2")] + // DBFS mount with mountpoint containing a sub-directory + [InlineData("dbfs", + "/mnt/x2/foo", + "https://ysa.dfs.core.windows.net/myx2/subdir/foo")] + //Azure SQL Non DBO Schema - [InlineData("sqlserver://purview-to-adb-sql.database.windows.net;database=purview-to-adb-sqldb;", "[mytest].[tablename.will.mark]", "mssql://purview-to-adb-sql.database.windows.net/purview-to-adb-sqldb/mytest/tablename.will.mark")] diff --git a/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/UnitTestData.cs b/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/UnitTestData.cs index 7b36a26..e39c4e5 100644 --- a/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/UnitTestData.cs +++ b/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/UnitTestData.cs @@ -25,7 +25,9 @@ public struct QnParserTestData new MountPoint(){MountPointName="/databricks-results",Source="databricks-results"}, new MountPoint(){MountPointName="/mnt/purview2/",Source="abfss://purview2@purviewexamplessa.dfs.core.windows.net/"}, new MountPoint(){MountPointName="/mnt/x/",Source="abfss://x@xsa.dfs.core.windows.net/"}, - new MountPoint(){MountPointName="/mnt/x/y",Source="abfss://y@ysa.dfs.core.windows.net/"} + new MountPoint(){MountPointName="/mnt/x/y",Source="abfss://y@ysa.dfs.core.windows.net/"}, + new MountPoint(){MountPointName="/mnt/x2/",Source="abfss://myx2@ysa.dfs.core.windows.net/subdir/"}, + new MountPoint(){MountPointName="/mnt/adlg1/",Source="adl://gen1.azuredatalakestore.net/subdir/"} }; } } From 7502ed5b24889a14fe34073458979f5fc12f8a44 Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Mon, 13 Mar 2023 01:28:51 -0500 Subject: [PATCH 42/59] Adding unit tests to confirm mount behavior --- .../Helpers/Parser/QnParserTests.cs | 16 ++++++++++++++++ .../Helpers/Parser/UnitTestData.cs | 1 + 2 files changed, 17 insertions(+) diff --git a/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs b/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs index 5ae75a2..cb2ee70 100644 --- a/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs +++ b/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs @@ -96,6 +96,22 @@ public QnParserTests() [InlineData("dbfs", "/mnt/x2/foo", "https://ysa.dfs.core.windows.net/myx2/subdir/foo")] + // DBFS mount with mountpoint containing a sub-directory for blob + [InlineData("dbfs", + "/mnt/blobx2/foo", + "https://ysa.blob.core.windows.net/myx2/subdir/foo")] + // DBFS mount containing a sub-directory + [InlineData("dbfs", + "/mnt/x2/retail", + "https://ysa.dfs.core.windows.net/myx2/subdir/retail")] + // DBFS mount - Shortest String Match containing a sub-directory + [InlineData("dbfs", + "/mnt/x2/abc", + "https://ysa.dfs.core.windows.net/myx2/subdir/abc")] + // DBFS mount - Longest String Match containing a sub-directory + [InlineData("dbfs", + "/mnt/x2/y/abc", + "https://ysa.dfs.core.windows.net/myx2/subdir/y/abc")] //Azure SQL Non DBO Schema - [InlineData("sqlserver://purview-to-adb-sql.database.windows.net;database=purview-to-adb-sqldb;", "[mytest].[tablename.will.mark]", diff --git a/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/UnitTestData.cs b/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/UnitTestData.cs index e39c4e5..7e436b9 100644 --- a/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/UnitTestData.cs +++ b/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/UnitTestData.cs @@ -27,6 +27,7 @@ public struct QnParserTestData new MountPoint(){MountPointName="/mnt/x/",Source="abfss://x@xsa.dfs.core.windows.net/"}, new MountPoint(){MountPointName="/mnt/x/y",Source="abfss://y@ysa.dfs.core.windows.net/"}, new MountPoint(){MountPointName="/mnt/x2/",Source="abfss://myx2@ysa.dfs.core.windows.net/subdir/"}, + new MountPoint(){MountPointName="/mnt/blobx2/",Source="wasbs://myx2@ysa.blob.core.windows.net/subdir/"}, new MountPoint(){MountPointName="/mnt/adlg1/",Source="adl://gen1.azuredatalakestore.net/subdir/"} }; } From c0f65d111e9082e3b7c11babbc72f867324bd7dc Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Fri, 10 Mar 2023 16:03:56 -0600 Subject: [PATCH 43/59] Testin --- function-app/adb-to-purview/src/Program.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/function-app/adb-to-purview/src/Program.cs b/function-app/adb-to-purview/src/Program.cs index e79b571..c5f0c21 100644 --- a/function-app/adb-to-purview/src/Program.cs +++ b/function-app/adb-to-purview/src/Program.cs @@ -25,7 +25,8 @@ public static void Main() workerApplication.UseMiddleware(); }) .ConfigureServices(s => - { + { + // Comment s.AddScoped(); s.AddScoped(); s.AddScoped(); From c9f01898392f03336ae63a0647182290d736ee18 Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Mon, 13 Mar 2023 00:36:28 -0500 Subject: [PATCH 44/59] Rollback Delta Merge statements due to false positive in test suite --- LIMITATIONS.md | 5 +- tests/integration/README.md | 1 + .../integration/jobdefs/spark3-tests-def.json | 30 ++++++ .../jobdefs/spark3-tests-expectations.json | 8 +- .../notebooks/delta-in-delta-merge-package.py | 94 +++++++++++++++++++ .../notebooks/delta-in-delta-merge.scala | 2 +- 6 files changed, 136 insertions(+), 4 deletions(-) create mode 100644 tests/integration/spark-apps/notebooks/delta-in-delta-merge-package.py diff --git a/LIMITATIONS.md b/LIMITATIONS.md index b392cad..b146169 100644 --- a/LIMITATIONS.md +++ b/LIMITATIONS.md @@ -71,7 +71,8 @@ Supports Azure SQL DB through the [Apache Spark Connector for Azure SQL DB](http Supports [Delta File Format](https://delta.io/). -* Supports MERGE INTO statement on Databricks Runtime 10.4 LTS and higher. +* Does NOT support MERGE INTO statement on Databricks due to differences in Databricsk and Open Source classes. + * An earlier release mistakenly indicated support * Does not support Delta on Spark 2 Databricks Runtimes. * Commands such as [Vacuum](https://docs.delta.io/latest/delta-utility.html#toc-entry-1) or [Optimize](https://docs.microsoft.com/en-us/azure/databricks/spark/latest/spark-sql/language-manual/delta-optimize) do not emit any lineage information and will not result in a Purview asset. @@ -156,5 +157,5 @@ Starting with OpenLineage 0.18.0 and release 2.3.0 of the solution accelerator, ### Column Mapping Support for Delta Format -* Delta Merge statements are supported when the table is stored in the default metastore +* Delta Merge statements are not supported at this time * Delta to Delta is NOT supported at this time diff --git a/tests/integration/README.md b/tests/integration/README.md index a44386b..a0704d9 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -20,6 +20,7 @@ A related `*-expectations.json` file should exist for every test definition. It |call-via-adf-spark2.scala|ABFS|ABFS|✅||Called via Azure Data Factory| |call-via-adf-spark3.scala|ABFS|ABFS||✅|Called via Azure Data Factory| |delta-in-delta-merge.scala|DELTA|DELTA|❌|❌|Uses a Merge Statement| +|delta-in-delta-merge-package.py|DELTA|DELTA|❌|❌|Uses a Merge Statement| |delta-in-delta-out-abfss.scala|DELTA|DELTA||✅|| |delta-in-delta-out-fs.scala|DELTA|DELTA||✅|| |delta-in-delta-out-mnt.scala|DELTA|DELTA||✅|Uses a Mount Point| diff --git a/tests/integration/jobdefs/spark3-tests-def.json b/tests/integration/jobdefs/spark3-tests-def.json index 1af96c1..a1d51fc 100644 --- a/tests/integration/jobdefs/spark3-tests-def.json +++ b/tests/integration/jobdefs/spark3-tests-def.json @@ -257,6 +257,36 @@ "existing_cluster_id": "", "timeout_seconds": 0, "email_notifications": {} + }, + { + "task_key": "delta-merge-task", + "depends_on": [ + { + "task_key": "mysql-in-mysql-out" + } + ], + "notebook_task": { + "notebook_path": "/Shared/examples/delta-in-delta-merge", + "source": "WORKSPACE" + }, + "existing_cluster_id": "", + "timeout_seconds": 0, + "email_notifications": {} + }, + { + "task_key": "delta-merge-pkg-task", + "depends_on": [ + { + "task_key": "delta-merge-task" + } + ], + "notebook_task": { + "notebook_path": "/Shared/examples/delta-in-delta-merge-package", + "source": "WORKSPACE" + }, + "existing_cluster_id": "", + "timeout_seconds": 0, + "email_notifications": {} } ], "format": "MULTI_TASK" diff --git a/tests/integration/jobdefs/spark3-tests-expectations.json b/tests/integration/jobdefs/spark3-tests-expectations.json index 37814c8..15960bb 100644 --- a/tests/integration/jobdefs/spark3-tests-expectations.json +++ b/tests/integration/jobdefs/spark3-tests-expectations.json @@ -52,5 +52,11 @@ "databricks://.azuredatabricks.net/jobs//tasks/wasbs-in-kusto-out/processes/D3E8F45D1D4150D809A56423DD2F2CFD->9D42C0F01CD2D2C2F014263C2D812602", "databricks://.azuredatabricks.net/notebooks/Shared/examples/kusto-in-wasbs-out", "databricks://.azuredatabricks.net/jobs//tasks/kusto-in-wasbs-out", - "databricks://.azuredatabricks.net/jobs//tasks/kusto-in-wasbs-out/processes/9D42C0F01CD2D2C2F014263C2D812602->55517236A8804C9548A4CB81814AEA6B" + "databricks://.azuredatabricks.net/jobs//tasks/kusto-in-wasbs-out/processes/9D42C0F01CD2D2C2F014263C2D812602->55517236A8804C9548A4CB81814AEA6B", + "databricks://.azuredatabricks.net/jobs//tasks/delta-merge-task", + "databricks://.azuredatabricks.net/jobs//tasks/delta-merge-task/processes/XXX->XXX", + "databricks://.azuredatabricks.net/notebooks/Shared/examples/delta-in-delta-merge", + "databricks://.azuredatabricks.net/jobs//tasks/delta-merge-pkg-task", + "databricks://.azuredatabricks.net/jobs//tasks/delta-merge-pkg-task/processes/XXX->XXX", + "databricks://.azuredatabricks.net/notebooks/Shared/examples/delta-in-delta-merge-package" ] \ No newline at end of file diff --git a/tests/integration/spark-apps/notebooks/delta-in-delta-merge-package.py b/tests/integration/spark-apps/notebooks/delta-in-delta-merge-package.py new file mode 100644 index 0000000..5acbb31 --- /dev/null +++ b/tests/integration/spark-apps/notebooks/delta-in-delta-merge-package.py @@ -0,0 +1,94 @@ +# Databricks notebook source +# MAGIC %md +# MAGIC # Delta Table using Python Package rather than SQL + +# COMMAND ---------- + +import os +storageServiceName = os.environ.get("STORAGE_SERVICE_NAME") +storageContainerName = "rawdata" +abfssRootPath = "abfss://"+storageContainerName+"@"+storageServiceName+".dfs.core.windows.net" + +storageKey = dbutils.secrets.get("purview-to-adb-kv", "storage-service-key") + +spark.conf.set("fs.azure.account.auth.type."+storageServiceName+".dfs.core.windows.net", "OAuth") +spark.conf.set("fs.azure.account.oauth.provider.type."+storageServiceName+".dfs.core.windows.net", "org.apache.hadoop.fs.azurebfs.oauth2.ClientCredsTokenProvider") +spark.conf.set("fs.azure.account.oauth2.client.id."+storageServiceName+".dfs.core.windows.net", dbutils.secrets.get("purview-to-adb-kv", "clientIdKey")) +spark.conf.set("fs.azure.account.oauth2.client.secret."+storageServiceName+".dfs.core.windows.net", dbutils.secrets.get("purview-to-adb-kv", "clientSecretKey")) +spark.conf.set("fs.azure.account.oauth2.client.endpoint."+storageServiceName+".dfs.core.windows.net", "https://login.microsoftonline.com/"+dbutils.secrets.get("purview-to-adb-kv", "tenant-id")+"/oauth2/token") + +# COMMAND ---------- + +from delta.tables import * + +exampleInputA = DeltaTable.forPath(spark, abfssRootPath+"/testcase/delta-merge-using-delta-package/subfolder-a/productA/") +exampleInputB = DeltaTable.forPath(spark, abfssRootPath+"/testcase/delta-merge-using-delta-package/subfolder-b/productB/") + +dfUpdates = exampleInputB.toDF() + +# COMMAND ---------- + +( + exampleInputA.alias('a') + .merge( + dfUpdates.alias('updates'), + 'a.id = updates.id' + ) + .whenMatchedUpdate( + set = { + "id": "updates.id", + "postalCode": "updates.postalCode", + "streetAddress": "updates.streetAddress" + } + ) + .whenNotMatchedInsert( + values = { + "id": "updates.id", + "postalCode": "updates.postalCode", + "streetAddress": "updates.streetAddress" + } + ) + .execute() +) + +# COMMAND ---------- + +# MAGIC %md +# MAGIC # For Experimenting + +# COMMAND ---------- + +# %scala +# val exampleA = ( +# spark.read.format("delta") +# .load(abfssRootPath+"/testcase/sixteen/exampleInputA") +# ) + +# val exampleB = ( +# spark.read.format("delta") +# .load(abfssRootPath+"/testcase/sixteen/exampleInputB") +# ) +# val outputDf = exampleA.join(exampleB, exampleA("id") === exampleB("id"), "inner").drop(exampleB("id")) +# outputDf.createOrReplaceTempView("outputDf") + +# COMMAND ---------- + +# dfUpdates.createOrReplaceTempView("updates") + +# COMMAND ---------- + +# %sql +# MERGE INTO deltadestination_tbl +# USING updates +# ON deltadestination_tbl.id = updates.id +# WHEN MATCHED THEN +# UPDATE SET +# id = updates.id, +# postalcode = updates.postalcode, +# streetAddress = updates.streetAddress +# WHEN NOT MATCHED +# THEN INSERT * + +# COMMAND ---------- + + diff --git a/tests/integration/spark-apps/notebooks/delta-in-delta-merge.scala b/tests/integration/spark-apps/notebooks/delta-in-delta-merge.scala index f2fc4ed..c74bc0e 100644 --- a/tests/integration/spark-apps/notebooks/delta-in-delta-merge.scala +++ b/tests/integration/spark-apps/notebooks/delta-in-delta-merge.scala @@ -35,7 +35,7 @@ outputDf.createOrReplaceTempView("outputDf") // COMMAND ---------- -// This is ran only once +// This is ran only once and screwing up our tests for delta // %sql // CREATE TABLE testcasesixteen // USING DELTA From 51678a0db099f9a97ee31c143922593746bf75a7 Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Mon, 13 Mar 2023 00:37:50 -0500 Subject: [PATCH 45/59] Remove unncessary comment --- function-app/adb-to-purview/src/Program.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/function-app/adb-to-purview/src/Program.cs b/function-app/adb-to-purview/src/Program.cs index c5f0c21..08943b6 100644 --- a/function-app/adb-to-purview/src/Program.cs +++ b/function-app/adb-to-purview/src/Program.cs @@ -25,8 +25,7 @@ public static void Main() workerApplication.UseMiddleware(); }) .ConfigureServices(s => - { - // Comment + { s.AddScoped(); s.AddScoped(); s.AddScoped(); From 6d8df7d35475bd6efdb58d8791db0513e6bbddf5 Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Mon, 13 Mar 2023 00:42:27 -0500 Subject: [PATCH 46/59] Update readme to rollback delta merge support --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a7c8535..e627e7e 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Gathering lineage data is performed in the following steps: * Azure Synapse Analytics (as input) * Azure Data Lake Gen 2 * Azure Blob Storage - * Delta Lake + * Delta Lake (Merge command not supported) * Azure Data Explorer * Azure Data Factory orchestration * Hive Tables (in default metastore) From c2bf9608213286ee7ccbd334b1027e0cd31e5451 Mon Sep 17 00:00:00 2001 From: Will Johnson Date: Mon, 13 Mar 2023 02:37:17 -0500 Subject: [PATCH 47/59] Mappings must be a separate artifact --- .github/workflows/build-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index c72af22..2af5975 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -85,7 +85,7 @@ jobs: - name: Upload One Line OlToPurviewMappings Build Artifact uses: actions/upload-artifact@v3 with: - name: FunctionZip + name: Mappings path: ~/artifact-mappings/one-line-mappings.json runIntegrationTests: From c73398bac428cf524b0c514f8a99c50676163ace Mon Sep 17 00:00:00 2001 From: Hanna Moazam Date: Thu, 22 Dec 2022 01:10:51 +0300 Subject: [PATCH 48/59] Implementing Cosmos support Rebased to include updates to newdeploymenttemp.json --- deployment/infra/OlToPurviewMappings.json | 13 +++ deployment/infra/newdeploymenttemp.json | 2 +- .../OlProcessing/OlMessageConsolodation.cs | 100 +++++++++++++++--- .../Helpers/OlProcessing/ValidateOlEvent.cs | 23 +++- 4 files changed, 122 insertions(+), 16 deletions(-) diff --git a/deployment/infra/OlToPurviewMappings.json b/deployment/infra/OlToPurviewMappings.json index b6918a2..23c3044 100644 --- a/deployment/infra/OlToPurviewMappings.json +++ b/deployment/infra/OlToPurviewMappings.json @@ -631,6 +631,19 @@ "qualifiedName": "https://{nameSpcBodyParts[0]}/{nameSpcBodyParts[1]}/{nameGroups[0]}", "purviewDataType": "azure_data_explorer_table", "purviewPrefix": "https" + }, + { + "name": "azureCosmos", + "parserConditions": [ + { + "op1": "prefix", + "compare": "=", + "op2": "azurecosmos" + } + ], + "qualifiedName": "https://{nameSpcBodyParts[0]}/{nameSpcBodyParts[1]}/{nameSpcBodyParts[2]}/{nameGroups[0]}", + "purviewDataType": "azure_cosmosdb_sqlapi_collection", + "purviewPrefix": "https" } ] } \ No newline at end of file diff --git a/deployment/infra/newdeploymenttemp.json b/deployment/infra/newdeploymenttemp.json index d82a238..cfe7d85 100644 --- a/deployment/infra/newdeploymenttemp.json +++ b/deployment/infra/newdeploymenttemp.json @@ -220,7 +220,7 @@ }, { "name": "OlToPurviewMappings", - "value": "{\"olToPurviewMappings\":[{\"name\":\"wasbsNLayerMnt\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"wasbs\"},{\"op1\":\"nameSpcBodyParts\",\"compare\":\">\",\"op2\":\"2\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_blob_path\",\"purviewPrefix\":\"https\"},{\"name\":\"wasbs\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"wasbs\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_blob_path\",\"purviewPrefix\":\"https\"},{\"name\":\"wasbNLayerMnt\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"wasb\"},{\"op1\":\"nameSpcBodyParts\",\"compare\":\">\",\"op2\":\"2\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_blob_path\",\"purviewPrefix\":\"https\"},{\"name\":\"wasb\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"wasb\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_blob_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsBlobRootFSNLayerMnt\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"},{\"op1\":\"nameSpcBodyParts\",\"compare\":\">\",\"op2\":\"2\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsBlobRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsRootFSNLayerMnt\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"},{\"op1\":\"nameSpcBodyParts\",\"compare\":\">\",\"op2\":\"2\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssBlobRootFSNLayerMnt\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"},{\"op1\":\"nameSpcBodyParts\",\"compare\":\">\",\"op2\":\"2\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssBlobRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssRootFSNLayerMnt\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"},{\"op1\":\"nameSpcBodyParts\",\"compare\":\">\",\"op2\":\"2\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsBlobNLayerMnt\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameSpcBodyParts\",\"compare\":\">\",\"op2\":\"2\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsBlob\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsNLayerMnt\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameSpcBodyParts\",\"compare\":\">\",\"op2\":\"2\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfs\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssBlobNLayerMnt\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameSpcBodyParts\",\"compare\":\">\",\"op2\":\"2\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssBlob\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssNLayerMnt\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameSpcBodyParts\",\"compare\":\">\",\"op2\":\"2\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameSpaceBodyJoinedBySlashFrom[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfss\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"synapseSqlNonDbo\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameSpcBodyParts[0]\",\"compare\":\"contains\",\"op2\":\"azuresynapse\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_synapse_dedicated_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"synapseSql\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameSpcBodyParts[0]\",\"compare\":\"contains\",\"op2\":\"azuresynapse\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/dbo/{nameGroups[0].parts[0]}\",\"purviewDataType\":\"azure_synapse_dedicated_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQLNonDbo\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameGroups\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0]}/{nameGroups[1]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQLNonDboNoDotsInNames\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQL\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/dbo/{nameGroups[0]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azurePostgresNonPublic\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"},{\"op1\":\"nameSpcConParts\",\"compare\":\">\",\"op2\":\"4\"},{\"op1\":\"nameSpcConParts[3]\",\"compare\":\"=\",\"op2\":\"azure\"}],\"qualifiedName\":\"postgresql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"azurePostgres\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameSpcConParts\",\"compare\":\">\",\"op2\":\"4\"},{\"op1\":\"nameSpcConParts[3]\",\"compare\":\"=\",\"op2\":\"azure\"}],\"qualifiedName\":\"postgresql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/public/{nameGroups[0]}\",\"purviewDataType\":\"azure_postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"postgresNonPublic\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"postgresql://servers/{nameSpcBodyParts[0]}:{nameSpcBodyParts[1]}/dbs/{nameSpcBodyParts[2]}/schemas/{nameGroups[0].parts[0]}/tables/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"postgres\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"}],\"qualifiedName\":\"postgresql://servers/{nameSpcBodyParts[0]}:{nameSpcBodyParts[1]}/dbs/{nameSpcBodyParts[2]}/schemas/public/tables/{nameGroups[0]}\",\"purviewDataType\":\"postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"hiveManagedTableNotDefault\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"dbfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"contains\",\"op2\":\"hive/warehouse\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"4\"}],\"qualifiedName\":\"{nameGroups[0].parts[3]}.{nameGroups[0].parts[5]}@{AdbWorkspaceUrl}\",\"purviewDataType\":\"hive_table\",\"purviewPrefix\":\"hive\"},{\"name\":\"hiveManagedTableDefault\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"dbfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"contains\",\"op2\":\"hive/warehouse\"}],\"qualifiedName\":\"default.{nameGroups[0].parts[3]}@{AdbWorkspaceUrl}\",\"purviewDataType\":\"hive_table\",\"purviewPrefix\":\"hive\"},{\"name\":\"azureMySql\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"mysql\"}],\"qualifiedName\":\"mysql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_mysql_table\",\"purviewPrefix\":\"mysql\"},{\"name\":\"kusto\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"azurekusto\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[0]}/{nameSpcBodyParts[1]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_data_explorer_table\",\"purviewPrefix\":\"https\"}]}" + "value": "{\"olToPurviewMappings\":[{\"name\":\"wasbs\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"wasbs\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_blob_path\",\"purviewPrefix\":\"https\"},{\"name\":\"wasb\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"wasb\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_blob_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsBlobRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssBlobRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssRootFS\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"=\",\"op2\":\"\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_filesystem\",\"purviewPrefix\":\"https\"},{\"name\":\"abfsBlob\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfs\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfs\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfssBlob\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"},{\"op1\":\"nameSpcBodyParts[1]\",\"compare\":\"contains\",\"op2\":\"blob\"}],\"qualifiedName\":\"https://{nameSpcConParts[0]}.dfs.{nameSpcConParts[2]}.{nameSpcConParts[3]}.{nameSpcConParts[4]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"abfss\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"abfss\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[1]}/{nameSpcBodyParts[0]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_datalake_gen2_path\",\"purviewPrefix\":\"https\"},{\"name\":\"synapseSqlNonDbo\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameSpcBodyParts[0]\",\"compare\":\"contains\",\"op2\":\"azuresynapse\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_synapse_dedicated_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"synapseSql\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameSpcBodyParts[0]\",\"compare\":\"contains\",\"op2\":\"azuresynapse\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/dbo/{nameGroups[0].parts[0]}\",\"purviewDataType\":\"azure_synapse_dedicated_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQLNonDbo\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameGroups\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0]}/{nameGroups[1]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQLNonDboNoDotsInNames\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azureSQL\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"sqlserver\"}],\"qualifiedName\":\"mssql://{nameSpcBodyParts[0]}/{nameSpcNameVals['database']}/dbo/{nameGroups[0]}\",\"purviewDataType\":\"azure_sql_table\",\"purviewPrefix\":\"mssql\"},{\"name\":\"azurePostgresNonPublic\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"},{\"op1\":\"nameSpcConParts\",\"compare\":\">\",\"op2\":\"4\"},{\"op1\":\"nameSpcConParts[3]\",\"compare\":\"=\",\"op2\":\"azure\"}],\"qualifiedName\":\"postgresql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/{nameGroups[0].parts[0]}/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"azure_postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"azurePostgres\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameSpcConParts\",\"compare\":\">\",\"op2\":\"4\"},{\"op1\":\"nameSpcConParts[3]\",\"compare\":\"=\",\"op2\":\"azure\"}],\"qualifiedName\":\"postgresql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/public/{nameGroups[0]}\",\"purviewDataType\":\"azure_postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"postgresNonPublic\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"1\"}],\"qualifiedName\":\"postgresql://servers/{nameSpcBodyParts[0]}:{nameSpcBodyParts[1]}/dbs/{nameSpcBodyParts[2]}/schemas/{nameGroups[0].parts[0]}/tables/{nameGroups[0].parts[1]}\",\"purviewDataType\":\"postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"postgres\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"postgresql\"}],\"qualifiedName\":\"postgresql://servers/{nameSpcBodyParts[0]}:{nameSpcBodyParts[1]}/dbs/{nameSpcBodyParts[2]}/schemas/public/tables/{nameGroups[0]}\",\"purviewDataType\":\"postgresql_table\",\"purviewPrefix\":\"postgresql\"},{\"name\":\"hiveManagedTableNotDefault\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"dbfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"contains\",\"op2\":\"hive/warehouse\"},{\"op1\":\"nameGroups[0].parts\",\"compare\":\">\",\"op2\":\"4\"}],\"qualifiedName\":\"{nameGroups[0].parts[3]}.{nameGroups[0].parts[5]}@{AdbWorkspaceUrl}\",\"purviewDataType\":\"hive_table\",\"purviewPrefix\":\"hive\"},{\"name\":\"hiveManagedTableDefault\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"dbfs\"},{\"op1\":\"nameGroups[0]\",\"compare\":\"contains\",\"op2\":\"hive/warehouse\"}],\"qualifiedName\":\"default.{nameGroups[0].parts[3]}@{AdbWorkspaceUrl}\",\"purviewDataType\":\"hive_table\",\"purviewPrefix\":\"hive\"},{\"name\":\"azureMySql\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"mysql\"}],\"qualifiedName\":\"mysql://{nameSpcBodyParts[0]}/{nameSpcBodyParts[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_mysql_table\",\"purviewPrefix\":\"mysql\"},{\"name\":\"kusto\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"azurekusto\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[0]}/{nameSpcBodyParts[1]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_data_explorer_table\",\"purviewPrefix\":\"https\"},{\"name\":\"azureCosmos\",\"parserConditions\":[{\"op1\":\"prefix\",\"compare\":\"=\",\"op2\":\"azurecosmos\"}],\"qualifiedName\":\"https://{nameSpcBodyParts[0]}/{nameSpcBodyParts[1]}/{nameSpcBodyParts[2]}/{nameGroups[0]}\",\"purviewDataType\":\"azure_cosmosdb_sqlapi_collection\",\"purviewPrefix\":\"https\"}]}" }, { "name": "PurviewAccountName", diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs index 2a4687b..576a44f 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs @@ -143,12 +143,26 @@ private async Task ProcessStartEvent(Event olEvent, string jobRunId, Envir } try { - var entity = new TableEntity(TABLE_PARTITION, olEvent.Run.RunId) + if (olEvent.Inputs.Count > 0) + // Store inputs and env facet. { - { "EnvFacet", JsonConvert.SerializeObject(olEvent.Run.Facets.EnvironmentProperties) } - }; + var entity = new TableEntity(TABLE_PARTITION, olEvent.Run.RunId) + { + { "EnvFacet", JsonConvert.SerializeObject(olEvent.Run.Facets.EnvironmentProperties) }, + { "Inputs", JsonConvert.SerializeObject(olEvent.Inputs) } + + }; + await _tableClient.AddEntityAsync(entity); + } + else { + // Store only env facet. + var entity = new TableEntity(TABLE_PARTITION, olEvent.Run.RunId) + { + { "EnvFacet", JsonConvert.SerializeObject(olEvent.Run.Facets.EnvironmentProperties) } - await _tableClient.AddEntityAsync(entity); + }; + await _tableClient.AddEntityAsync(entity); + } } catch (RequestFailedException ex) { @@ -159,6 +173,7 @@ private async Task ProcessStartEvent(Event olEvent, string jobRunId, Envir _log.LogError(ex, $"OlMessageConsolodation-ProcessStartEvent: Error {ex.Message} when processing entity"); return false; } + return true; } @@ -170,6 +185,7 @@ private async Task JoinEventData(Event olEvent, string jobRunId) } TableEntity te; + TableEntity te_inputs; // Processing time can sometimes cause complete events int retryCount = 4; @@ -195,6 +211,28 @@ private async Task JoinEventData(Event olEvent, string jobRunId) await Task.Delay(delay); } + // Get inputs. Todo: Check if more efficient to get inputs within the same while loop above. Can we get 2 entities at the same time? + currentRetry = 0; + while (true) + { + try + { + _log.LogInformation("Trying to get inputs"); + te_inputs = await _tableClient.GetEntityAsync(TABLE_PARTITION, olEvent.Run.RunId, new string[] { "Inputs" }); + break; + } + catch (RequestFailedException) + { + currentRetry++; + _log.LogWarning($"Start event was missing, retrying to consolidate message to get inputs. Retry count: {currentRetry}"); + if (currentRetry > retryCount) + { + return false; + } + } + await Task.Delay(delay); + } + // Add Environment to event var envFacet = JsonConvert.DeserializeObject(te["EnvFacet"].ToString() ?? ""); if (envFacet is null) @@ -204,15 +242,28 @@ private async Task JoinEventData(Event olEvent, string jobRunId) } olEvent.Run.Facets.EnvironmentProperties = envFacet; - // clean up table over time - try - { - var delresp = await _tableClient.DeleteEntityAsync(TABLE_PARTITION, olEvent.Run.RunId); - } - catch (Exception ex) - { - _log.LogError(ex, $"OlMessageConsolodation-JoinEventData: Error {ex.Message} when deleting entity"); - } + // Add Inputs to event if not already there (will only be done for DataSourceV2 sources) + if (olEvent.Inputs.Count == 0) { + var inputs = JsonConvert.DeserializeObject>(te_inputs["Inputs"].ToString() ?? ""); + + if (inputs is null) + { + _log.LogWarning($"OlMessageConsolodation-JoinEventData: Warning: no inputs found for datasource v2 COMPLETE event"); + return false; + } + olEvent.Inputs = inputs; + + } + + // clean up table over time. + try + { + var delresp = await _tableClient.DeleteEntityAsync(TABLE_PARTITION, olEvent.Run.RunId); + } + catch (Exception ex) + { + _log.LogError(ex, $"OlMessageConsolodation-JoinEventData: Error {ex.Message} when deleting entity"); + } return true; } @@ -228,11 +279,32 @@ private bool IsStartEventEnvironment(Event olEvent) return false; } + /// + /// Helper function to determine if the event is one of + /// the data source v2 ones which need to aggregate data + /// from the start and complete events + /// + private bool isDataSourceV2Event(Event olEvent) { + string[] special_cases = {"azurecosmos://", "iceberg://"}; // todo: make this configurable? + // Don't need to process START events here as they have both inputs and outputs + if (olEvent.EventType == "START") return false; + + foreach (var outp in olEvent.Outputs) + { + foreach (var source in special_cases) + { + if (outp.NameSpace.StartsWith(source)) return true; + } + } + return false; + } + private bool IsJoinEvent(Event olEvent) { + string[] special_cases = {"cosmos", "iceberg"}; if (olEvent.EventType == COMPLETE_EVENT) { - if (olEvent.Inputs.Count > 0 && olEvent.Outputs.Count > 0) + if ((olEvent.Inputs.Count > 0 && olEvent.Outputs.Count > 0) || (olEvent.Outputs.Count > 0 && isDataSourceV2Event(olEvent))) { return true; } diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/ValidateOlEvent.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/ValidateOlEvent.cs index 984eb3b..74486f0 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/ValidateOlEvent.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/ValidateOlEvent.cs @@ -29,10 +29,31 @@ public ValidateOlEvent(ILoggerFactory loggerFactory) _log = loggerFactory.CreateLogger(); } + /// + /// Helper function to determine if the event is one of + /// the data source v2 ones which need to aggregate data + /// from the start and complete events + /// + private bool isDataSourceV2Event(Event olEvent) { + string[] special_cases = {"azurecosmos://", "iceberg://"}; // todo: make this configurable? + // Don't need to process START events here as they have both inputs and outputs + if (olEvent.EventType == "START") return false; + + foreach (var outp in olEvent.Outputs) + { + foreach (var source in special_cases) + { + if (outp.NameSpace.StartsWith(source)) return true; + } + } + return false; + } + /// /// Performs initial validation of OpenLineage input /// The tested criteria include: /// 1. Events have both inputs and outputs + /// a. Except for special cases covered in isDataSourceV2Event /// 2. Events do not have the same input and output /// 3. EventType is START or COMPLETE /// 4. If EventType is START, there is a Environment Facet @@ -40,7 +61,7 @@ public ValidateOlEvent(ILoggerFactory loggerFactory) /// OpenLineage Event message /// true if input is valid, false if not public bool Validate(Event olEvent){ - if (olEvent.Inputs.Count > 0 && olEvent.Outputs.Count > 0) + if ((olEvent.Inputs.Count > 0 && olEvent.Outputs.Count > 0) || (olEvent.Outputs.Count > 0 && isDataSourceV2Event(olEvent))) { // Need to rework for multiple inputs and outputs in one packet - possibly combine and then hash if (InOutEqual(olEvent)) From ba6c4aa038766e9a3bb60a254c0e07233b047348 Mon Sep 17 00:00:00 2001 From: Hanna Moazam Date: Mon, 16 Jan 2023 00:47:38 +0300 Subject: [PATCH 49/59] Cosmos integration test added, and test-env README updated. TODO: Update LIMITATIONS, add integration test notebooks, and add integration test for additional inputs + cosmos --- LIMITATIONS.md | 1 + .../Helpers/Parser/QnParserTests.cs | 4 + tests/environment/README.md | 7 +- tests/environment/sources/cosmos.bicep | 160 ++++++++++++++++++ 4 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 tests/environment/sources/cosmos.bicep diff --git a/LIMITATIONS.md b/LIMITATIONS.md index b146169..bf591b6 100644 --- a/LIMITATIONS.md +++ b/LIMITATIONS.md @@ -13,6 +13,7 @@ The solution accelerator supports a limited set of data sources to be ingested i * [Azure MySQL](#azure-mysql) * [PostgreSQL](#postgresql) * [Azure Data Explorer](#azure-data-explorer) +* [Azure Cosmos DB]() * [Other Data Sources and Limitations](#other-data-sources-and-limitations) * [Column Level Mapping Supported Sources](#column-level-mapping-supported-sources) diff --git a/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs b/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs index cb2ee70..8e813a7 100644 --- a/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs +++ b/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs @@ -148,6 +148,10 @@ public QnParserTests() [InlineData("azurekusto://qpll4l5hchczm.eastus2.kusto.windows.net/database01", "table01", "https://qpll4l5hchczm.eastus2.kusto.windows.net/database01/table01")] + // Cosmos + [InlineData("azurecosmos://6ch4pkm5tpniq.documents.azure.com/dbs/myDatabase", + "/colls/yourContainer", + "https://6ch4pkm5tpniq.documents.azure.com/dbs/mydatabase/colls/yourcontainer")] public void GetIdentifiers_OlSource_ReturnsPurviewIdentifier(string nameSpace, string name, string expectedResult) { diff --git a/tests/environment/README.md b/tests/environment/README.md index ee76c63..4f3714a 100644 --- a/tests/environment/README.md +++ b/tests/environment/README.md @@ -50,6 +50,8 @@ Add Key Vault Secrets * `azurekusto-appsecret` * `azurekusto-uri` + * `azurecosmos-endpoint` + * `azurecosmos-key` * Update SQL Db and Synapse Server with AAD Admin * Add Service Principal for Databricks to connect to SQL sources * Assign the Service Principal admin role on the ADX cluster. [Guide](https://learn.microsoft.com/en-us/azure/data-explorer/provision-azure-ad-app#grant-the-service-principal-access-to-an-azure-data-explorer-database) @@ -63,9 +65,12 @@ Set the following system environments: Install the version of the [kusto spark connector](https://github.com/Azure/azure-kusto-spark) that matches the cluster Scala and Spark versions from Maven Central. Upload notebooks in `./tests/integration/spark-apps/notebooks/` to dbfs' `/Shared/examples/` - * Manually for now. TODO: Automate this in Python +Install the following libraries on the compute cluster (versions to match the Spark and Scala versions of the cluster) (TODO: Automate): +* Cosmos spark connector + + Compile the following apps and upload them to `/dbfs/FileStore/testcases/` * `./tests/integration/spark-apps/jarjobs/abfssInAbfssOut/` with `./gradlew build` diff --git a/tests/environment/sources/cosmos.bicep b/tests/environment/sources/cosmos.bicep new file mode 100644 index 0000000..bf9bcba --- /dev/null +++ b/tests/environment/sources/cosmos.bicep @@ -0,0 +1,160 @@ +@description('Azure Cosmos DB account name, max length 44 characters') +param accountName string = uniqueString('cosmos', resourceGroup().id) + +@description('Location for the Azure Cosmos DB account.') +param location string = resourceGroup().location + +@allowed([ + 'Eventual' + 'ConsistentPrefix' + 'Session' + 'BoundedStaleness' + 'Strong' +]) +@description('The default consistency level of the Cosmos DB account.') +param defaultConsistencyLevel string = 'Session' + +@minValue(10) +@maxValue(2147483647) +@description('Max stale requests. Required for BoundedStaleness. Valid ranges, Single Region: 10 to 2147483647. Multi Region: 100000 to 2147483647.') +param maxStalenessPrefix int = 100000 + +@minValue(5) +@maxValue(86400) +@description('Max lag time (minutes). Required for BoundedStaleness. Valid ranges, Single Region: 5 to 84600. Multi Region: 300 to 86400.') +param maxIntervalInSeconds int = 300 + +@allowed([ + true + false +]) +@description('Enable system managed failover for regions') +param systemManagedFailover bool = false + +@description('The name for the database') +param databaseName string = 'myDatabase' + +@description('The name for the input container') +param inContainerName string = 'myContainer' + +@description('The name for the output container') +param outContainerName string = 'yourContainer' + +@minValue(400) +@maxValue(1000000) +@description('The throughput for the container') +param throughput int = 400 + +var consistencyPolicy = { + Eventual: { + defaultConsistencyLevel: 'Eventual' + } + ConsistentPrefix: { + defaultConsistencyLevel: 'ConsistentPrefix' + } + Session: { + defaultConsistencyLevel: 'Session' + } + BoundedStaleness: { + defaultConsistencyLevel: 'BoundedStaleness' + maxStalenessPrefix: maxStalenessPrefix + maxIntervalInSeconds: maxIntervalInSeconds + } + Strong: { + defaultConsistencyLevel: 'Strong' + } +} +var locations = [ + { + locationName: location + failoverPriority: 0 + isZoneRedundant: false + } +] + +resource account 'Microsoft.DocumentDB/databaseAccounts@2022-05-15' = { + name: toLower(accountName) + location: location + kind: 'GlobalDocumentDB' + properties: { + consistencyPolicy: consistencyPolicy[defaultConsistencyLevel] + locations: locations + databaseAccountOfferType: 'Standard' + enableAutomaticFailover: systemManagedFailover + } +} + +resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2022-05-15' = { + name: '${account.name}/${databaseName}' + properties: { + resource: { + id: databaseName + } + } +} + +resource container 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2022-05-15' = { + name: '${database.name}/${inContainerName}' + properties: { + resource: { + id: inContainerName + partitionKey: { + paths: [ + '/country' + ] + kind: 'Hash' + } + indexingPolicy: { + indexingMode: 'consistent' + automatic: true + includedPaths: [ + { + path: '/*' + } + ] + excludedPaths: [ + { + path: '/\'_etag\'/?' + } + ] + } + defaultTtl: 86400 + } + options: { + throughput: throughput + } + } +} + +resource container2 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2022-05-15' = { + name: '${database.name}/${outContainerName}' + properties: { + resource: { + id: outContainerName + partitionKey: { + paths: [ + '/country' + ] + kind: 'Hash' + } + indexingPolicy: { + indexingMode: 'consistent' + automatic: true + includedPaths: [ + { + path: '/*' + } + ] + excludedPaths: [ + { + path: '/\'_etag\'/?' + } + ] + } + defaultTtl: 86400 + } + options: { + throughput: throughput + } + } +} From 7f24fa267b0b0fc07dfc233303f4d530b9907400 Mon Sep 17 00:00:00 2001 From: Hanna Moazam Date: Wed, 18 Jan 2023 01:21:26 +0300 Subject: [PATCH 50/59] WIP - found flaws in DataSourceV2 events logic --- .../OlProcessing/OlMessageConsolodation.cs | 38 ++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs index 576a44f..46559c5 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs @@ -149,6 +149,9 @@ private async Task ProcessStartEvent(Event olEvent, string jobRunId, Envir var entity = new TableEntity(TABLE_PARTITION, olEvent.Run.RunId) { { "EnvFacet", JsonConvert.SerializeObject(olEvent.Run.Facets.EnvironmentProperties) }, + // TODO: Add logic here to only save DataSourceV2 event inputs. Old logic of checking the outputs + // for datasourceV2 events is not right because the issue is with inputs, and we may miss them if + // the output was not a datasource v2 event. { "Inputs", JsonConvert.SerializeObject(olEvent.Inputs) } }; @@ -217,7 +220,6 @@ private async Task JoinEventData(Event olEvent, string jobRunId) { try { - _log.LogInformation("Trying to get inputs"); te_inputs = await _tableClient.GetEntityAsync(TABLE_PARTITION, olEvent.Run.RunId, new string[] { "Inputs" }); break; } @@ -242,18 +244,37 @@ private async Task JoinEventData(Event olEvent, string jobRunId) } olEvent.Run.Facets.EnvironmentProperties = envFacet; - // Add Inputs to event if not already there (will only be done for DataSourceV2 sources) - if (olEvent.Inputs.Count == 0) { - var inputs = JsonConvert.DeserializeObject>(te_inputs["Inputs"].ToString() ?? ""); + // Check if saved any inputs from the START event (will only be done for events containing DataSourceV2 sources) + if (te_inputs is not null) { + var saved_inputs = JsonConvert.DeserializeObject>(te_inputs["Inputs"].ToString() ?? ""); - if (inputs is null) - { + if (saved_inputs is null) { _log.LogWarning($"OlMessageConsolodation-JoinEventData: Warning: no inputs found for datasource v2 COMPLETE event"); return false; - } - olEvent.Inputs = inputs; + } + + // Check inputs saved against inputs captured in this COMPLETE event and combine, then remove any duplicates. + // Checking for duplicates may be overkill because we observed that DataSource V2 COMPLETE events do not show up in the inputs + // of the COMPLETE event, so in theory we would only have saved DataSource V2 inputs from the START event, and they would + // not show up in the COMPLETE event + var inputs = new List(saved_inputs.Count + olEvent.Inputs.Count); + + inputs.AddRange(saved_inputs); + inputs.AddRange(olEvent.Inputs); + + // TODO: Come back to this } + // if (olEvent.Inputs.Count == 0) { + // var inputs = JsonConvert.DeserializeObject>(te_inputs["Inputs"].ToString() ?? ""); + + // if (inputs is null) + // { + // _log.LogWarning($"OlMessageConsolodation-JoinEventData: Warning: no inputs found for datasource v2 COMPLETE event"); + // return false; + // } + olEvent.Inputs = inputs; + // } // clean up table over time. try @@ -301,7 +322,6 @@ private bool isDataSourceV2Event(Event olEvent) { private bool IsJoinEvent(Event olEvent) { - string[] special_cases = {"cosmos", "iceberg"}; if (olEvent.EventType == COMPLETE_EVENT) { if ((olEvent.Inputs.Count > 0 && olEvent.Outputs.Count > 0) || (olEvent.Outputs.Count > 0 && isDataSourceV2Event(olEvent))) From 9f3714c464679067ed96fb828a69464748cb3c70 Mon Sep 17 00:00:00 2001 From: Hanna Moazam Date: Wed, 18 Jan 2023 19:34:14 +0300 Subject: [PATCH 51/59] Cosmos support WIP --- .../OlProcessing/OlMessageConsolodation.cs | 47 ++++++++----------- .../Models/Parser/OpenLineage/Inputs.cs | 14 ++++++ 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs index 46559c5..7557378 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Linq; using System.Threading.Tasks; using System.Threading; using System.Collections.Generic; @@ -143,15 +144,12 @@ private async Task ProcessStartEvent(Event olEvent, string jobRunId, Envir } try { - if (olEvent.Inputs.Count > 0) + if (isDataSourceV2Event(olEvent)) // Store inputs and env facet. { var entity = new TableEntity(TABLE_PARTITION, olEvent.Run.RunId) { { "EnvFacet", JsonConvert.SerializeObject(olEvent.Run.Facets.EnvironmentProperties) }, - // TODO: Add logic here to only save DataSourceV2 event inputs. Old logic of checking the outputs - // for datasourceV2 events is not right because the issue is with inputs, and we may miss them if - // the output was not a datasource v2 event. { "Inputs", JsonConvert.SerializeObject(olEvent.Inputs) } }; @@ -186,7 +184,7 @@ private async Task JoinEventData(Event olEvent, string jobRunId) { return false; } - + TableEntity te; TableEntity te_inputs; @@ -253,28 +251,15 @@ private async Task JoinEventData(Event olEvent, string jobRunId) return false; } - // Check inputs saved against inputs captured in this COMPLETE event and combine, then remove any duplicates. - // Checking for duplicates may be overkill because we observed that DataSource V2 COMPLETE events do not show up in the inputs - // of the COMPLETE event, so in theory we would only have saved DataSource V2 inputs from the START event, and they would - // not show up in the COMPLETE event + // Check inputs saved against inputs captured in this COMPLETE event and combine while removing any duplicates. + // Checking for duplicates needed since we save all the inputs captured from the START event. Perhaps it may be better to + // only save the DataSourceV2 inputs? var inputs = new List(saved_inputs.Count + olEvent.Inputs.Count); - inputs.AddRange(saved_inputs); inputs.AddRange(olEvent.Inputs); - - // TODO: Come back to this - + var unique_inputs = inputs.Distinct(); + olEvent.Inputs = unique_inputs.ToList(); } - // if (olEvent.Inputs.Count == 0) { - // var inputs = JsonConvert.DeserializeObject>(te_inputs["Inputs"].ToString() ?? ""); - - // if (inputs is null) - // { - // _log.LogWarning($"OlMessageConsolodation-JoinEventData: Warning: no inputs found for datasource v2 COMPLETE event"); - // return false; - // } - olEvent.Inputs = inputs; - // } // clean up table over time. try @@ -286,6 +271,12 @@ private async Task JoinEventData(Event olEvent, string jobRunId) _log.LogError(ex, $"OlMessageConsolodation-JoinEventData: Error {ex.Message} when deleting entity"); } + // If no inputs were saved from the start event, then we need to make sure we're only processing this COMPLETE event + // if it has both inputs and outputs (reflects original logic, prior to supporting DataSourceV2 events) + if (te_inputs is null && !(olEvent.Inputs.Count > 0 && olEvent.Outputs.Count > 0)) { + return false; + } + return true; } @@ -302,13 +293,13 @@ private bool IsStartEventEnvironment(Event olEvent) /// /// Helper function to determine if the event is one of - /// the data source v2 ones which need to aggregate data - /// from the start and complete events + /// the data source v2 ones which needs us to save the + /// inputs from the start event /// private bool isDataSourceV2Event(Event olEvent) { string[] special_cases = {"azurecosmos://", "iceberg://"}; // todo: make this configurable? - // Don't need to process START events here as they have both inputs and outputs - if (olEvent.EventType == "START") return false; + // // Don't need to process START events here as they have both inputs and outputs + // if (olEvent.EventType == "COMPLETE") return false; foreach (var outp in olEvent.Outputs) { @@ -324,7 +315,7 @@ private bool IsJoinEvent(Event olEvent) { if (olEvent.EventType == COMPLETE_EVENT) { - if ((olEvent.Inputs.Count > 0 && olEvent.Outputs.Count > 0) || (olEvent.Outputs.Count > 0 && isDataSourceV2Event(olEvent))) + if (olEvent.Outputs.Count > 0) { return true; } diff --git a/function-app/adb-to-purview/src/Function.Domain/Models/Parser/OpenLineage/Inputs.cs b/function-app/adb-to-purview/src/Function.Domain/Models/Parser/OpenLineage/Inputs.cs index 4f15245..85ba05e 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Models/Parser/OpenLineage/Inputs.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Models/Parser/OpenLineage/Inputs.cs @@ -10,5 +10,19 @@ public class Inputs: IInputsOutputs public string Name { get; set; } = ""; [JsonProperty("namespace")] public string NameSpace { get; set; } = ""; + + public override bool Equals(object obj) { + if (obj is Inputs other) + { + if (Name == other.Name && NameSpace == other.NameSpace) + return true; + } + return false; + } + + public override int GetHashCode() { + return Name.GetHashCode() ^ + NameSpace.GetHashCode(); + } } } \ No newline at end of file From b67bbe189c0ce4a152bc8d520acb63791883d466 Mon Sep 17 00:00:00 2001 From: Hanna Moazam Date: Wed, 18 Jan 2023 19:40:25 +0300 Subject: [PATCH 52/59] Cosmos WIP --- .../Helpers/OlProcessing/OlMessageConsolodation.cs | 2 -- .../Function.Domain/Helpers/Parser/QnParserTests.cs | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs index 7557378..d588d15 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs @@ -298,8 +298,6 @@ private bool IsStartEventEnvironment(Event olEvent) /// private bool isDataSourceV2Event(Event olEvent) { string[] special_cases = {"azurecosmos://", "iceberg://"}; // todo: make this configurable? - // // Don't need to process START events here as they have both inputs and outputs - // if (olEvent.EventType == "COMPLETE") return false; foreach (var outp in olEvent.Outputs) { diff --git a/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs b/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs index 8e813a7..aa0e9f1 100644 --- a/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs +++ b/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Helpers/Parser/QnParserTests.cs @@ -149,8 +149,8 @@ public QnParserTests() "table01", "https://qpll4l5hchczm.eastus2.kusto.windows.net/database01/table01")] // Cosmos - [InlineData("azurecosmos://6ch4pkm5tpniq.documents.azure.com/dbs/myDatabase", - "/colls/yourContainer", + [InlineData("azurecosmos://6ch4pkm5tpniq.documents.azure.com/dbs/mydatabase", + "/colls/yourcontainer", "https://6ch4pkm5tpniq.documents.azure.com/dbs/mydatabase/colls/yourcontainer")] public void GetIdentifiers_OlSource_ReturnsPurviewIdentifier(string nameSpace, string name, string expectedResult) From 50f31296e889605f067fff589614a0415f057931 Mon Sep 17 00:00:00 2001 From: Hanna Moazam Date: Wed, 18 Jan 2023 20:20:30 +0300 Subject: [PATCH 53/59] Cosmos WIP --- .../Helpers/OlProcessing/ValidateOlEvent.cs | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/ValidateOlEvent.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/ValidateOlEvent.cs index 74486f0..3e4cbf8 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/ValidateOlEvent.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/ValidateOlEvent.cs @@ -29,22 +29,20 @@ public ValidateOlEvent(ILoggerFactory loggerFactory) _log = loggerFactory.CreateLogger(); } - /// +/// /// Helper function to determine if the event is one of - /// the data source v2 ones which need to aggregate data - /// from the start and complete events + /// the data source v2 ones which needs us to save the + /// inputs from the start event /// private bool isDataSourceV2Event(Event olEvent) { string[] special_cases = {"azurecosmos://", "iceberg://"}; // todo: make this configurable? - // Don't need to process START events here as they have both inputs and outputs - if (olEvent.EventType == "START") return false; foreach (var outp in olEvent.Outputs) { foreach (var source in special_cases) { if (outp.NameSpace.StartsWith(source)) return true; - } + } } return false; } @@ -52,8 +50,7 @@ private bool isDataSourceV2Event(Event olEvent) { /// /// Performs initial validation of OpenLineage input /// The tested criteria include: - /// 1. Events have both inputs and outputs - /// a. Except for special cases covered in isDataSourceV2Event + /// 1. Events have both inputs and outputs (TODO: UPDATE) /// 2. Events do not have the same input and output /// 3. EventType is START or COMPLETE /// 4. If EventType is START, there is a Environment Facet @@ -61,7 +58,9 @@ private bool isDataSourceV2Event(Event olEvent) { /// OpenLineage Event message /// true if input is valid, false if not public bool Validate(Event olEvent){ - if ((olEvent.Inputs.Count > 0 && olEvent.Outputs.Count > 0) || (olEvent.Outputs.Count > 0 && isDataSourceV2Event(olEvent))) + // if ((olEvent.Inputs.Count > 0 && olEvent.Outputs.Count > 0) || (olEvent.Outputs.Count > 0 && isDataSourceV2Event(olEvent))) + if (olEvent.Outputs.Count > 0) // TODO: check if this breaks any logic down the line. + // Want to save COMPLETE events even if they only have outputs for the cosmos case { // Need to rework for multiple inputs and outputs in one packet - possibly combine and then hash if (InOutEqual(olEvent)) @@ -70,14 +69,14 @@ public bool Validate(Event olEvent){ } if (olEvent.EventType == "START") { - if (olEvent.Run.Facets.EnvironmentProperties == null) - { + if (olEvent.Run.Facets.EnvironmentProperties == null || !(olEvent.Inputs.Count > 0 && olEvent.Outputs.Count > 0)) + { // START events should contain both inputs and outputs, as well as the EnvironmentProperties facet return false; } return true; } - else if (olEvent.EventType == "COMPLETE") - { + else if (olEvent.EventType == "COMPLETE" && olEvent.Outputs.Count > 0) + { // COMPLETE events might not contain inputs, but should have at least one output. return true; } else From 00b18d44e9d9907d8a4af2a81214a799687bfc14 Mon Sep 17 00:00:00 2001 From: Hanna Moazam Date: Wed, 18 Jan 2023 20:31:32 +0300 Subject: [PATCH 54/59] Update LIMITATIONS --- LIMITATIONS.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/LIMITATIONS.md b/LIMITATIONS.md index bf591b6..5aed8b9 100644 --- a/LIMITATIONS.md +++ b/LIMITATIONS.md @@ -10,10 +10,14 @@ The solution accelerator supports a limited set of data sources to be ingested i * [Azure Synapse SQL Pools](#azure-synapse-sql-pools) * [Azure SQL DB](#azure-sql-db) * [Delta Lake](#delta-lake-file-format) +<<<<<<< HEAD * [Azure MySQL](#azure-mysql) * [PostgreSQL](#postgresql) * [Azure Data Explorer](#azure-data-explorer) * [Azure Cosmos DB]() +======= +* [Azure Cosmos DB](#azure-cosmos-db) +>>>>>>> 85ddab3 (Update LIMITATIONS) * [Other Data Sources and Limitations](#other-data-sources-and-limitations) * [Column Level Mapping Supported Sources](#column-level-mapping-supported-sources) @@ -105,6 +109,10 @@ Supports capturing lineage for Databricks Notebook activities in Azure Data Fact * At this time, the Microsoft Purview view of Azure Data Factory lineage will not contain these tasks unless the Databricks Task uses or feeds a data source to a Data Flow or Copy activity. * Copy Activities may not show lineage connecting to these Databricks tasks since it emits individual file assets rather than folder or resource set assets. +## Azure Cosmos DB + +Supports querying [Azure Cosmos DB (SQL API)](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/cosmos/azure-cosmos-spark_3_2-12) + ## Other Data Sources and Limitations ### Lineage for Unsupported Data Sources From 247e56a07e8e5314001680c55056428f3ca4e822 Mon Sep 17 00:00:00 2001 From: Hanna Moazam Date: Wed, 18 Jan 2023 20:40:30 +0300 Subject: [PATCH 55/59] Updated UnitTestData, test CompleteNoOutputsInputsFullMessage expected result to True, as now considering COMPLETE events with only outputs --- .../tests/unit-tests/Function.Domain/Services/UnitTestData.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Services/UnitTestData.cs b/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Services/UnitTestData.cs index 21416b3..0248993 100644 --- a/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Services/UnitTestData.cs +++ b/function-app/adb-to-purview/tests/unit-tests/Function.Domain/Services/UnitTestData.cs @@ -17,7 +17,7 @@ public IEnumerator GetEnumerator() , false}; // CompleteNoOutputsInputsFullMessage yield return new object[] {"CompleteNoOutputsInputsFullMessage: 2022-01-12T00:05:56.318 [Information] OpenLineageIn:{\"eventType\":\"COMPLETE\",\"eventTime\":\"2022-01-25T17:52:53.363Z\",\"inputs\":[],\"outputs\":[{\"namespace\":\"dbfs\",\"name\":\"/mnt/raw/DimProduct.parquet\"}],\"producer\":\"https://github.com/OpenLineage/OpenLineage/tree/0.5.0-SNAPSHOT/integration/spark\",\"schemaURL\":\"https://openlineage.io/spec/1-0-2/OpenLineage.json#/$defs/RunEvent\"}" - , false}; + , true}; // CompleteOutputsAndInputsFullMessage yield return new object[] {"CompleteOutputsAndInputsFullMessage: 2022-01-12T00:19:41.550 [Information] OpenLineageIn:{\"eventType\":\"COMPLETE\",\"eventTime\":\"2022-01-25T17:52:53.363Z\",\"inputs\":[{\"namespace\":\"dbfs\",\"name\":\"/mnt/raw/DimProduct.parquet\"}],\"outputs\":[{\"namespace\":\"dbfs\",\"name\":\"/mnt/destination/DimProduct.parquet\"}],\"producer\":\"https://github.com/OpenLineage/OpenLineage/tree/0.5.0-SNAPSHOT/integration/spark\",\"schemaURL\":\"https://openlineage.io/spec/1-0-2/OpenLineage.json#/$defs/RunEvent\"}" , true}; From c3bbfdda2a10311b4944475155e1c0131c977349 Mon Sep 17 00:00:00 2001 From: Hanna Moazam Date: Sun, 29 Jan 2023 10:10:11 +0300 Subject: [PATCH 56/59] save progress - VS code broken --- .../OlProcessing/OlMessageConsolodation.cs | 4 ++-- .../Helpers/OlProcessing/ValidateOlEvent.cs | 24 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs index d588d15..709e41d 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs @@ -299,11 +299,11 @@ private bool IsStartEventEnvironment(Event olEvent) private bool isDataSourceV2Event(Event olEvent) { string[] special_cases = {"azurecosmos://", "iceberg://"}; // todo: make this configurable? - foreach (var outp in olEvent.Outputs) + foreach (var inp in olEvent.Inputs) { foreach (var source in special_cases) { - if (outp.NameSpace.StartsWith(source)) return true; + if (inp.NameSpace.StartsWith(source)) return true; } } return false; diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/ValidateOlEvent.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/ValidateOlEvent.cs index 3e4cbf8..1c7ae53 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/ValidateOlEvent.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/ValidateOlEvent.cs @@ -29,23 +29,23 @@ public ValidateOlEvent(ILoggerFactory loggerFactory) _log = loggerFactory.CreateLogger(); } -/// + /// /// Helper function to determine if the event is one of /// the data source v2 ones which needs us to save the /// inputs from the start event /// - private bool isDataSourceV2Event(Event olEvent) { - string[] special_cases = {"azurecosmos://", "iceberg://"}; // todo: make this configurable? + // private bool isDataSourceV2Event(Event olEvent) { + // string[] special_cases = {"azurecosmos://", "iceberg://"}; // todo: make this configurable? - foreach (var outp in olEvent.Outputs) - { - foreach (var source in special_cases) - { - if (outp.NameSpace.StartsWith(source)) return true; - } - } - return false; - } + // foreach (var outp in olEvent.Outputs) + // { + // foreach (var source in special_cases) + // { + // if (outp.NameSpace.StartsWith(source)) return true; + // } + // } + // return false; + // } /// /// Performs initial validation of OpenLineage input From e008feb16d843576539ad04a5f8957001c72b133 Mon Sep 17 00:00:00 2001 From: Hanna Moazam Date: Mon, 6 Feb 2023 19:52:38 +0300 Subject: [PATCH 57/59] Cosmos WIP --- .../Helpers/OlProcessing/OlMessageConsolodation.cs | 1 + function-app/adb-to-purview/src/adb-to-purview.csproj | 4 ++-- .../QualifiedNameConfigTester.csproj | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs index 709e41d..bad1d84 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs @@ -244,6 +244,7 @@ private async Task JoinEventData(Event olEvent, string jobRunId) // Check if saved any inputs from the START event (will only be done for events containing DataSourceV2 sources) if (te_inputs is not null) { + // TODO: Possible source of error. var saved_inputs = JsonConvert.DeserializeObject>(te_inputs["Inputs"].ToString() ?? ""); if (saved_inputs is null) { diff --git a/function-app/adb-to-purview/src/adb-to-purview.csproj b/function-app/adb-to-purview/src/adb-to-purview.csproj index 489e48f..88a44a2 100644 --- a/function-app/adb-to-purview/src/adb-to-purview.csproj +++ b/function-app/adb-to-purview/src/adb-to-purview.csproj @@ -14,10 +14,10 @@ - + - + diff --git a/function-app/adb-to-purview/tests/tools/QualifiedNameConfigTester/QualifiedNameConfigTester.csproj b/function-app/adb-to-purview/tests/tools/QualifiedNameConfigTester/QualifiedNameConfigTester.csproj index fbe03c6..2673c90 100644 --- a/function-app/adb-to-purview/tests/tools/QualifiedNameConfigTester/QualifiedNameConfigTester.csproj +++ b/function-app/adb-to-purview/tests/tools/QualifiedNameConfigTester/QualifiedNameConfigTester.csproj @@ -7,8 +7,8 @@ enable - - + + From 2da877e8a175bd7afe3211622e962f6fbffaf5af Mon Sep 17 00:00:00 2001 From: Hanna Moazam Date: Mon, 13 Feb 2023 01:43:35 +0300 Subject: [PATCH 58/59] Unsuccessful debugging --- .../OlProcessing/OlMessageConsolodation.cs | 71 +++++++++++++------ .../Services/OlConsolodateEnrich.cs | 8 +++ 2 files changed, 58 insertions(+), 21 deletions(-) diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs index bad1d84..2c2eb5f 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs @@ -185,15 +185,17 @@ private async Task JoinEventData(Event olEvent, string jobRunId) return false; } - TableEntity te; - TableEntity te_inputs; + TableEntity te = null; + TableEntity te_inputs = null; + + bool ret_val = true; // Processing time can sometimes cause complete events int retryCount = 4; int currentRetry = 0; TimeSpan delay = TimeSpan.FromSeconds(1); - while (true) + while (ret_val) { try { @@ -206,7 +208,8 @@ private async Task JoinEventData(Event olEvent, string jobRunId) _log.LogWarning($"Start event was missing, retrying to consolidate message. Retry count: {currentRetry}"); if (currentRetry > retryCount) { - return false; + ret_val = false; + break; } } await Task.Delay(delay); @@ -214,7 +217,7 @@ private async Task JoinEventData(Event olEvent, string jobRunId) // Get inputs. Todo: Check if more efficient to get inputs within the same while loop above. Can we get 2 entities at the same time? currentRetry = 0; - while (true) + while (ret_val) // ret_val instead of just true, because if didn't have the env_facet then don't bother getting inputs either { try { @@ -227,7 +230,8 @@ private async Task JoinEventData(Event olEvent, string jobRunId) _log.LogWarning($"Start event was missing, retrying to consolidate message to get inputs. Retry count: {currentRetry}"); if (currentRetry > retryCount) { - return false; + ret_val = false; + break; } } await Task.Delay(delay); @@ -238,28 +242,53 @@ private async Task JoinEventData(Event olEvent, string jobRunId) if (envFacet is null) { _log.LogWarning($"OlMessageConsolodation-JoinEventData: Warning environment facet for COMPLETE event is null"); - return false; + ret_val = false; } olEvent.Run.Facets.EnvironmentProperties = envFacet; // Check if saved any inputs from the START event (will only be done for events containing DataSourceV2 sources) if (te_inputs is not null) { // TODO: Possible source of error. - var saved_inputs = JsonConvert.DeserializeObject>(te_inputs["Inputs"].ToString() ?? ""); + if (te_inputs.ContainsKey("Inputs")){ + _log.LogInformation($"New Code #1"); + try { + var saved_inputs = JsonConvert.DeserializeObject>(te_inputs["Inputs"].ToString() ?? ""); + _log.LogInformation($"New Code #1.1"); + _log.LogInformation("Inputs in dictionary? ", te_inputs.ContainsKey("Inputs").ToString()); + + if (saved_inputs is null) { + // Unecessary check? + _log.LogInformation($"OlMessageConsolodation-JoinEventData: No inputs found for COMPLETE event"); + //ret_val = false; + } + + else { + // Check inputs saved against inputs captured in this COMPLETE event and combine while removing any duplicates. + // Checking for duplicates needed since we save all the inputs captured from the START event. Perhaps it may be better to + // only save the DataSourceV2 inputs? + var inputs = new List(saved_inputs.Count + olEvent.Inputs.Count); + inputs.AddRange(saved_inputs); + inputs.AddRange(olEvent.Inputs); + var unique_inputs = inputs.Distinct(); + olEvent.Inputs = unique_inputs.ToList(); + _log.LogInformation($"OlMessageConsolodation-JoinEventData: Captured inputs for COMPLETE event"); + } + } + catch (System.Exception ex) { + _log.LogError(ex, $"OlMessageConsolodation-JoinEventData: Error {ex.Message} when deserializing inputs"); + ret_val = false; + } + - if (saved_inputs is null) { - _log.LogWarning($"OlMessageConsolodation-JoinEventData: Warning: no inputs found for datasource v2 COMPLETE event"); - return false; + } - // Check inputs saved against inputs captured in this COMPLETE event and combine while removing any duplicates. - // Checking for duplicates needed since we save all the inputs captured from the START event. Perhaps it may be better to - // only save the DataSourceV2 inputs? - var inputs = new List(saved_inputs.Count + olEvent.Inputs.Count); - inputs.AddRange(saved_inputs); - inputs.AddRange(olEvent.Inputs); - var unique_inputs = inputs.Distinct(); - olEvent.Inputs = unique_inputs.ToList(); + else { + _log.LogInformation($"New Code #2"); + _log.LogInformation($"OlMessageConsolodation-JoinEventData: No inputs found for COMPLETE event"); + //ret_val = false; + } + } // clean up table over time. @@ -275,10 +304,10 @@ private async Task JoinEventData(Event olEvent, string jobRunId) // If no inputs were saved from the start event, then we need to make sure we're only processing this COMPLETE event // if it has both inputs and outputs (reflects original logic, prior to supporting DataSourceV2 events) if (te_inputs is null && !(olEvent.Inputs.Count > 0 && olEvent.Outputs.Count > 0)) { - return false; + ret_val = false; } - return true; + return ret_val; } // Returns true if olEvent is of type START and has the environment facet diff --git a/function-app/adb-to-purview/src/Function.Domain/Services/OlConsolodateEnrich.cs b/function-app/adb-to-purview/src/Function.Domain/Services/OlConsolodateEnrich.cs index b0868c6..4280003 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Services/OlConsolodateEnrich.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Services/OlConsolodateEnrich.cs @@ -93,18 +93,26 @@ public OlConsolodateEnrich( // consolidate and enrich the complete event if possible else if (_event.EventType == COMPLETE_EVENT_TYPE) { + _logger.LogInformation("is COMPLETE_EVENT_TYPE #1"); var consolodatedEvent = await olMessageConsolodation.ConsolodateCompleteEvent(_event, _event.Run.RunId); if (consolodatedEvent == null) { + _logger.LogInformation("is COMPLETE_EVENT_TYPE #2"); return null; } else { + _logger.LogInformation("is COMPLETE_EVENT_TYPE #3"); var enrichedEvent = await olEnrichMessage.GetEnrichedEvent(consolodatedEvent); + _logger.LogInformation("is COMPLETE_EVENT_TYPE #4"); + if (enrichedEvent == null) { + _logger.LogInformation("is COMPLETE_EVENT_TYPE #5"); return null; } + _logger.LogInformation("is COMPLETE_EVENT_TYPE #6"); + return enrichedEvent; } } From edd56575dc7d7f84b77a0ae98697ac111dba1f0a Mon Sep 17 00:00:00 2001 From: Hanna Moazam Date: Fri, 17 Feb 2023 18:55:27 +0300 Subject: [PATCH 59/59] Fix null error when accessing inputs from table storage, and clean up code --- .../OlProcessing/OlMessageConsolodation.cs | 69 ++++++++----------- .../Helpers/OlProcessing/ValidateOlEvent.cs | 33 +++------ .../Services/OlConsolodateEnrich.cs | 8 +-- 3 files changed, 39 insertions(+), 71 deletions(-) diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs index 2c2eb5f..5e8dc48 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/OlMessageConsolodation.cs @@ -215,9 +215,9 @@ private async Task JoinEventData(Event olEvent, string jobRunId) await Task.Delay(delay); } - // Get inputs. Todo: Check if more efficient to get inputs within the same while loop above. Can we get 2 entities at the same time? + // Get inputs. TODO: Check if more efficient to get inputs within the same while loop above. Can we get 2 entities at the same time? currentRetry = 0; - while (ret_val) // ret_val instead of just true, because if didn't have the env_facet then don't bother getting inputs either + while (ret_val) // use a variable instead of just true, because if we didn't have the env_facet then we don't need to get inputs { try { @@ -248,30 +248,29 @@ private async Task JoinEventData(Event olEvent, string jobRunId) // Check if saved any inputs from the START event (will only be done for events containing DataSourceV2 sources) if (te_inputs is not null) { - // TODO: Possible source of error. - if (te_inputs.ContainsKey("Inputs")){ - _log.LogInformation($"New Code #1"); + + if (te_inputs.ContainsKey("Inputs")) { try { - var saved_inputs = JsonConvert.DeserializeObject>(te_inputs["Inputs"].ToString() ?? ""); - _log.LogInformation($"New Code #1.1"); - _log.LogInformation("Inputs in dictionary? ", te_inputs.ContainsKey("Inputs").ToString()); - - if (saved_inputs is null) { - // Unecessary check? - _log.LogInformation($"OlMessageConsolodation-JoinEventData: No inputs found for COMPLETE event"); - //ret_val = false; - } - - else { - // Check inputs saved against inputs captured in this COMPLETE event and combine while removing any duplicates. - // Checking for duplicates needed since we save all the inputs captured from the START event. Perhaps it may be better to - // only save the DataSourceV2 inputs? - var inputs = new List(saved_inputs.Count + olEvent.Inputs.Count); - inputs.AddRange(saved_inputs); - inputs.AddRange(olEvent.Inputs); - var unique_inputs = inputs.Distinct(); - olEvent.Inputs = unique_inputs.ToList(); - _log.LogInformation($"OlMessageConsolodation-JoinEventData: Captured inputs for COMPLETE event"); + //TODO: Find out why inputs might be null. Technically inputs are only added to the table if they exist. This is also not an issue when running locally. + if (te_inputs["Inputs"] != null) { + + var saved_inputs = JsonConvert.DeserializeObject>(te_inputs["Inputs"].ToString() ?? ""); + + if (saved_inputs is null) { + _log.LogInformation($"OlMessageConsolodation-JoinEventData: No inputs found for COMPLETE event"); + } + + else { + // Check inputs saved against inputs captured in this COMPLETE event and combine while removing any duplicates. + // Checking for duplicates needed since we save all the inputs captured from the START event. Perhaps it may be better to + // only save the DataSourceV2 inputs? + var inputs = new List(saved_inputs.Count + olEvent.Inputs.Count); + inputs.AddRange(saved_inputs); + inputs.AddRange(olEvent.Inputs); + var unique_inputs = inputs.Distinct(); + olEvent.Inputs = unique_inputs.ToList(); + _log.LogInformation($"OlMessageConsolodation-JoinEventData: Captured inputs for COMPLETE event"); + } } } catch (System.Exception ex) { @@ -279,14 +278,6 @@ private async Task JoinEventData(Event olEvent, string jobRunId) ret_val = false; } - - - } - - else { - _log.LogInformation($"New Code #2"); - _log.LogInformation($"OlMessageConsolodation-JoinEventData: No inputs found for COMPLETE event"); - //ret_val = false; } } @@ -301,9 +292,9 @@ private async Task JoinEventData(Event olEvent, string jobRunId) _log.LogError(ex, $"OlMessageConsolodation-JoinEventData: Error {ex.Message} when deleting entity"); } - // If no inputs were saved from the start event, then we need to make sure we're only processing this COMPLETE event - // if it has both inputs and outputs (reflects original logic, prior to supporting DataSourceV2 events) - if (te_inputs is null && !(olEvent.Inputs.Count > 0 && olEvent.Outputs.Count > 0)) { + // Need to make sure we're only processing this COMPLETE event if it has both + // inputs and outputs (reflects original logic, prior to supporting DataSourceV2 events) + if (!(olEvent.Inputs.Count > 0 && olEvent.Outputs.Count > 0)) { ret_val = false; } @@ -314,9 +305,9 @@ private async Task JoinEventData(Event olEvent, string jobRunId) private bool IsStartEventEnvironment(Event olEvent) { if (olEvent.EventType == START_EVENT && olEvent.Run.Facets.EnvironmentProperties != null) - { - return true; - } + { + return true; + } return false; } diff --git a/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/ValidateOlEvent.cs b/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/ValidateOlEvent.cs index 1c7ae53..7a554ea 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/ValidateOlEvent.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Helpers/OlProcessing/ValidateOlEvent.cs @@ -29,28 +29,10 @@ public ValidateOlEvent(ILoggerFactory loggerFactory) _log = loggerFactory.CreateLogger(); } - /// - /// Helper function to determine if the event is one of - /// the data source v2 ones which needs us to save the - /// inputs from the start event - /// - // private bool isDataSourceV2Event(Event olEvent) { - // string[] special_cases = {"azurecosmos://", "iceberg://"}; // todo: make this configurable? - - // foreach (var outp in olEvent.Outputs) - // { - // foreach (var source in special_cases) - // { - // if (outp.NameSpace.StartsWith(source)) return true; - // } - // } - // return false; - // } - /// /// Performs initial validation of OpenLineage input /// The tested criteria include: - /// 1. Events have both inputs and outputs (TODO: UPDATE) + /// 1. Events have outputs (not both inputs and outputs, because in the case of DataSourceV2 events, the COMPLETE event will not have inputs) /// 2. Events do not have the same input and output /// 3. EventType is START or COMPLETE /// 4. If EventType is START, there is a Environment Facet @@ -58,25 +40,26 @@ public ValidateOlEvent(ILoggerFactory loggerFactory) /// OpenLineage Event message /// true if input is valid, false if not public bool Validate(Event olEvent){ - // if ((olEvent.Inputs.Count > 0 && olEvent.Outputs.Count > 0) || (olEvent.Outputs.Count > 0 && isDataSourceV2Event(olEvent))) - if (olEvent.Outputs.Count > 0) // TODO: check if this breaks any logic down the line. - // Want to save COMPLETE events even if they only have outputs for the cosmos case + if (olEvent.Outputs.Count > 0) + // Want to save COMPLETE events even if they only have outputs, to deal with cosmos { // Need to rework for multiple inputs and outputs in one packet - possibly combine and then hash if (InOutEqual(olEvent)) - { + { return false; } if (olEvent.EventType == "START") { + // START events should contain both inputs and outputs, as well as the EnvironmentProperties facet if (olEvent.Run.Facets.EnvironmentProperties == null || !(olEvent.Inputs.Count > 0 && olEvent.Outputs.Count > 0)) - { // START events should contain both inputs and outputs, as well as the EnvironmentProperties facet + { return false; } return true; } + // COMPLETE events might not contain inputs, but should have at least one output. else if (olEvent.EventType == "COMPLETE" && olEvent.Outputs.Count > 0) - { // COMPLETE events might not contain inputs, but should have at least one output. + { return true; } else diff --git a/function-app/adb-to-purview/src/Function.Domain/Services/OlConsolodateEnrich.cs b/function-app/adb-to-purview/src/Function.Domain/Services/OlConsolodateEnrich.cs index 4280003..193f2aa 100644 --- a/function-app/adb-to-purview/src/Function.Domain/Services/OlConsolodateEnrich.cs +++ b/function-app/adb-to-purview/src/Function.Domain/Services/OlConsolodateEnrich.cs @@ -93,26 +93,20 @@ public OlConsolodateEnrich( // consolidate and enrich the complete event if possible else if (_event.EventType == COMPLETE_EVENT_TYPE) { - _logger.LogInformation("is COMPLETE_EVENT_TYPE #1"); var consolodatedEvent = await olMessageConsolodation.ConsolodateCompleteEvent(_event, _event.Run.RunId); if (consolodatedEvent == null) { - _logger.LogInformation("is COMPLETE_EVENT_TYPE #2"); return null; } else { - _logger.LogInformation("is COMPLETE_EVENT_TYPE #3"); var enrichedEvent = await olEnrichMessage.GetEnrichedEvent(consolodatedEvent); - _logger.LogInformation("is COMPLETE_EVENT_TYPE #4"); if (enrichedEvent == null) { - _logger.LogInformation("is COMPLETE_EVENT_TYPE #5"); return null; } - _logger.LogInformation("is COMPLETE_EVENT_TYPE #6"); - + return enrichedEvent; } }