mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-09-20 09:32:28 +01:00
Merge branch 'master' of https://github.com/Azure/cosmos-explorer into users/aisayas/sql-connectionstring-login
This commit is contained in:
+38
-108
@@ -101,13 +101,13 @@ jobs:
|
|||||||
- name: "Az CLI login"
|
- name: "Az CLI login"
|
||||||
uses: azure/login@v1
|
uses: azure/login@v1
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
client-id: ${{ secrets.E2ETESTS_CLIENT_ID }}
|
||||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
tenant-id: ${{ secrets.E2ETESTS_TENANT_ID }}
|
||||||
subscription-id: ${{ secrets.PREVIEW_SUBSCRIPTION_ID }}
|
subscription-id: ${{ secrets.E2ETESTS_SUBSCRIPTION_ID }}
|
||||||
- name: Upload build to preview blob storage
|
- name: Upload build to preview blob storage
|
||||||
run: az storage blob upload-batch -d '$web' -s 'dist' --account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} --destination-path "${{github.event.pull_request.head.sha || github.sha}}" --auth-mode login --overwrite true
|
run: az storage blob upload-batch -d '$web' -s 'dist' --account-name ${{ secrets.E2ETESTS_STORAGEACCOUNT_NAME }} --destination-path "${{github.event.pull_request.head.sha || github.sha}}" --auth-mode login --overwrite true
|
||||||
- name: Upload preview config to blob storage
|
- name: Upload preview config to blob storage
|
||||||
run: az storage blob upload -c '$web' -f ./preview/config.json --account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --auth-mode login --overwrite true
|
run: az storage blob upload -c '$web' -f ./preview/config.json --account-name ${{ secrets.E2ETESTS_STORAGEACCOUNT_NAME }} --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --auth-mode login --overwrite true
|
||||||
nuget:
|
nuget:
|
||||||
name: Publish Nuget
|
name: Publish Nuget
|
||||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||||
@@ -162,7 +162,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
AZURE_SUBSCRIPTION_ID: ${{ secrets.E2ETESTS_SUBSCRIPTION_ID }}
|
||||||
|
AZURE_TENANT_ID: ${{ secrets.E2ETESTS_TENANT_ID }}
|
||||||
|
DE_ACCOUNT_PREFIX: ${{ secrets.E2ETESTS_ACCOUNT_PREFIX }}
|
||||||
|
DE_TEST_RESOURCE_GROUP: ${{ secrets.E2ETESTS_RESOURCEGROUP_NAME }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -179,120 +182,47 @@ jobs:
|
|||||||
- name: "Az CLI login"
|
- name: "Az CLI login"
|
||||||
uses: Azure/login@v2
|
uses: Azure/login@v2
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }}
|
client-id: ${{ secrets.E2ETESTS_CLIENT_ID }}
|
||||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
tenant-id: ${{ secrets.E2ETESTS_TENANT_ID }}
|
||||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
subscription-id: ${{ secrets.E2ETESTS_SUBSCRIPTION_ID }}
|
||||||
# We can't use MSAL within playwright so we acquire tokens prior to running the tests
|
# We can't use MSAL within playwright so we acquire tokens prior to running the tests
|
||||||
- name: "Acquire RBAC tokens for test accounts"
|
- name: "Acquire RBAC tokens for test accounts"
|
||||||
uses: azure/cli@v2
|
uses: azure/cli@v2
|
||||||
|
env:
|
||||||
|
DE_ACCOUNT_PREFIX: ${{ secrets.E2ETESTS_ACCOUNT_PREFIX }}
|
||||||
|
DE_TEST_RESOURCE_GROUP: ${{ secrets.E2ETESTS_RESOURCEGROUP_NAME }}
|
||||||
with:
|
with:
|
||||||
azcliversion: latest
|
azcliversion: latest
|
||||||
inlineScript: |
|
inlineScript: |
|
||||||
SHARD_INDEX=${{ matrix.shardIndex }}
|
SHARD_INDEX=${{ matrix.shardIndex }}
|
||||||
echo PLAYWRIGHT_SHARD_INDEX=$SHARD_INDEX >> $GITHUB_ENV
|
echo PLAYWRIGHT_SHARD_INDEX=$SHARD_INDEX >> $GITHUB_ENV
|
||||||
|
|
||||||
NOSQL_TESTACCOUNT_1_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-1.documents.azure.com/.default" -o tsv --query accessToken)
|
NOSQL_TESTACCOUNT=${DE_ACCOUNT_PREFIX}-de-test-sql-${SHARD_INDEX}.documents.azure.com
|
||||||
echo "::add-mask::$NOSQL_TESTACCOUNT_1_TOKEN"
|
NOSQL_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://$NOSQL_TESTACCOUNT/.default" -o tsv --query accessToken)
|
||||||
echo NOSQL_TESTACCOUNT_1_TOKEN=$NOSQL_TESTACCOUNT_1_TOKEN >> $GITHUB_ENV
|
echo "::add-mask::$NOSQL_TESTACCOUNT_TOKEN"
|
||||||
|
echo NOSQL_TESTACCOUNT_TOKEN=$NOSQL_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
NOSQL_TESTACCOUNT_2_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-2.documents.azure.com/.default" -o tsv --query accessToken)
|
NOSQL_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://$DE_ACCOUNT_PREFIX-de-test-sql-readonly.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
echo "::add-mask::$NOSQL_TESTACCOUNT_2_TOKEN"
|
|
||||||
echo NOSQL_TESTACCOUNT_2_TOKEN=$NOSQL_TESTACCOUNT_2_TOKEN >> $GITHUB_ENV
|
|
||||||
|
|
||||||
NOSQL_TESTACCOUNT_3_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-3.documents.azure.com/.default" -o tsv --query accessToken)
|
|
||||||
echo "::add-mask::$NOSQL_TESTACCOUNT_3_TOKEN"
|
|
||||||
echo NOSQL_TESTACCOUNT_3_TOKEN=$NOSQL_TESTACCOUNT_3_TOKEN >> $GITHUB_ENV
|
|
||||||
|
|
||||||
NOSQL_TESTACCOUNT_4_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-4.documents.azure.com/.default" -o tsv --query accessToken)
|
|
||||||
echo "::add-mask::$NOSQL_TESTACCOUNT_4_TOKEN"
|
|
||||||
echo NOSQL_TESTACCOUNT_4_TOKEN=$NOSQL_TESTACCOUNT_4_TOKEN >> $GITHUB_ENV
|
|
||||||
|
|
||||||
NOSQL_TESTACCOUNT_5_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-5.documents.azure.com/.default" -o tsv --query accessToken)
|
|
||||||
echo "::add-mask::$NOSQL_TESTACCOUNT_5_TOKEN"
|
|
||||||
echo NOSQL_TESTACCOUNT_5_TOKEN=$NOSQL_TESTACCOUNT_5_TOKEN >> $GITHUB_ENV
|
|
||||||
|
|
||||||
NOSQL_TESTACCOUNT_6_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-6.documents.azure.com/.default" -o tsv --query accessToken)
|
|
||||||
echo "::add-mask::$NOSQL_TESTACCOUNT_6_TOKEN"
|
|
||||||
echo NOSQL_TESTACCOUNT_6_TOKEN=$NOSQL_TESTACCOUNT_6_TOKEN >> $GITHUB_ENV
|
|
||||||
|
|
||||||
NOSQL_TESTACCOUNT_7_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-7.documents.azure.com/.default" -o tsv --query accessToken)
|
|
||||||
echo "::add-mask::$NOSQL_TESTACCOUNT_7_TOKEN"
|
|
||||||
echo NOSQL_TESTACCOUNT_7_TOKEN=$NOSQL_TESTACCOUNT_7_TOKEN >> $GITHUB_ENV
|
|
||||||
|
|
||||||
NOSQL_TESTACCOUNT_8_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-8.documents.azure.com/.default" -o tsv --query accessToken)
|
|
||||||
echo "::add-mask::$NOSQL_TESTACCOUNT_8_TOKEN"
|
|
||||||
echo NOSQL_TESTACCOUNT_8_TOKEN=$NOSQL_TESTACCOUNT_8_TOKEN >> $GITHUB_ENV
|
|
||||||
|
|
||||||
NOSQL_TESTACCOUNT_9_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-9.documents.azure.com/.default" -o tsv --query accessToken)
|
|
||||||
echo "::add-mask::$NOSQL_TESTACCOUNT_9_TOKEN"
|
|
||||||
echo NOSQL_TESTACCOUNT_9_TOKEN=$NOSQL_TESTACCOUNT_9_TOKEN >> $GITHUB_ENV
|
|
||||||
|
|
||||||
NOSQL_TESTACCOUNT_10_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-10.documents.azure.com/.default" -o tsv --query accessToken)
|
|
||||||
echo "::add-mask::$NOSQL_TESTACCOUNT_10_TOKEN"
|
|
||||||
echo NOSQL_TESTACCOUNT_10_TOKEN=$NOSQL_TESTACCOUNT_10_TOKEN >> $GITHUB_ENV
|
|
||||||
|
|
||||||
NOSQL_TESTACCOUNT_11_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-11.documents.azure.com/.default" -o tsv --query accessToken)
|
|
||||||
echo "::add-mask::$NOSQL_TESTACCOUNT_11_TOKEN"
|
|
||||||
echo NOSQL_TESTACCOUNT_11_TOKEN=$NOSQL_TESTACCOUNT_11_TOKEN >> $GITHUB_ENV
|
|
||||||
|
|
||||||
NOSQL_TESTACCOUNT_12_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-12.documents.azure.com/.default" -o tsv --query accessToken)
|
|
||||||
echo "::add-mask::$NOSQL_TESTACCOUNT_12_TOKEN"
|
|
||||||
echo NOSQL_TESTACCOUNT_12_TOKEN=$NOSQL_TESTACCOUNT_12_TOKEN >> $GITHUB_ENV
|
|
||||||
|
|
||||||
NOSQL_TESTACCOUNT_13_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-13.documents.azure.com/.default" -o tsv --query accessToken)
|
|
||||||
echo "::add-mask::$NOSQL_TESTACCOUNT_13_TOKEN"
|
|
||||||
echo NOSQL_TESTACCOUNT_13_TOKEN=$NOSQL_TESTACCOUNT_13_TOKEN >> $GITHUB_ENV
|
|
||||||
|
|
||||||
NOSQL_TESTACCOUNT_14_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-14.documents.azure.com/.default" -o tsv --query accessToken)
|
|
||||||
echo "::add-mask::$NOSQL_TESTACCOUNT_14_TOKEN"
|
|
||||||
echo NOSQL_TESTACCOUNT_14_TOKEN=$NOSQL_TESTACCOUNT_14_TOKEN >> $GITHUB_ENV
|
|
||||||
|
|
||||||
NOSQL_TESTACCOUNT_15_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-15.documents.azure.com/.default" -o tsv --query accessToken)
|
|
||||||
echo "::add-mask::$NOSQL_TESTACCOUNT_15_TOKEN"
|
|
||||||
echo NOSQL_TESTACCOUNT_15_TOKEN=$NOSQL_TESTACCOUNT_15_TOKEN >> $GITHUB_ENV
|
|
||||||
|
|
||||||
NOSQL_TESTACCOUNT_16_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-16.documents.azure.com/.default" -o tsv --query accessToken)
|
|
||||||
echo "::add-mask::$NOSQL_TESTACCOUNT_16_TOKEN"
|
|
||||||
echo NOSQL_TESTACCOUNT_16_TOKEN=$NOSQL_TESTACCOUNT_16_TOKEN >> $GITHUB_ENV
|
|
||||||
|
|
||||||
NOSQL_TESTACCOUNT_17_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-17.documents.azure.com/.default" -o tsv --query accessToken)
|
|
||||||
echo "::add-mask::$NOSQL_TESTACCOUNT_17_TOKEN"
|
|
||||||
echo NOSQL_TESTACCOUNT_17_TOKEN=$NOSQL_TESTACCOUNT_17_TOKEN >> $GITHUB_ENV
|
|
||||||
|
|
||||||
NOSQL_TESTACCOUNT_18_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-18.documents.azure.com/.default" -o tsv --query accessToken)
|
|
||||||
echo "::add-mask::$NOSQL_TESTACCOUNT_18_TOKEN"
|
|
||||||
echo NOSQL_TESTACCOUNT_18_TOKEN=$NOSQL_TESTACCOUNT_18_TOKEN >> $GITHUB_ENV
|
|
||||||
|
|
||||||
NOSQL_TESTACCOUNT_19_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-19.documents.azure.com/.default" -o tsv --query accessToken)
|
|
||||||
echo "::add-mask::$NOSQL_TESTACCOUNT_19_TOKEN"
|
|
||||||
echo NOSQL_TESTACCOUNT_19_TOKEN=$NOSQL_TESTACCOUNT_19_TOKEN >> $GITHUB_ENV
|
|
||||||
|
|
||||||
NOSQL_TESTACCOUNT_20_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-20.documents.azure.com/.default" -o tsv --query accessToken)
|
|
||||||
echo "::add-mask::$NOSQL_TESTACCOUNT_20_TOKEN"
|
|
||||||
echo NOSQL_TESTACCOUNT_20_TOKEN=$NOSQL_TESTACCOUNT_20_TOKEN >> $GITHUB_ENV
|
|
||||||
|
|
||||||
NOSQL_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-readonly.documents.azure.com/.default" -o tsv --query accessToken)
|
|
||||||
echo "::add-mask::$NOSQL_READONLY_TESTACCOUNT_TOKEN"
|
echo "::add-mask::$NOSQL_READONLY_TESTACCOUNT_TOKEN"
|
||||||
echo NOSQL_READONLY_TESTACCOUNT_TOKEN=$NOSQL_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
echo NOSQL_READONLY_TESTACCOUNT_TOKEN=$NOSQL_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-containercopyonly.documents.azure.com/.default" -o tsv --query accessToken)
|
NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://${DE_ACCOUNT_PREFIX}-de-test-sql-containercopy.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
echo "::add-mask::$NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN"
|
echo "::add-mask::$NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN"
|
||||||
echo NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=$NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
echo NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=$NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
TABLE_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-tables.documents.azure.com/.default" -o tsv --query accessToken)
|
TABLE_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://${DE_ACCOUNT_PREFIX}-de-test-table-1.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
echo "::add-mask::$TABLE_TESTACCOUNT_TOKEN"
|
echo "::add-mask::$TABLE_TESTACCOUNT_TOKEN"
|
||||||
echo TABLE_TESTACCOUNT_TOKEN=$TABLE_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
echo TABLE_TESTACCOUNT_TOKEN=$TABLE_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
GREMLIN_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-gremlin.documents.azure.com/.default" -o tsv --query accessToken)
|
GREMLIN_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://${DE_ACCOUNT_PREFIX}-de-test-gremlin-1.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
echo "::add-mask::$GREMLIN_TESTACCOUNT_TOKEN"
|
echo "::add-mask::$GREMLIN_TESTACCOUNT_TOKEN"
|
||||||
echo GREMLIN_TESTACCOUNT_TOKEN=$GREMLIN_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
echo GREMLIN_TESTACCOUNT_TOKEN=$GREMLIN_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
CASSANDRA_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-cassandra.documents.azure.com/.default" -o tsv --query accessToken)
|
CASSANDRA_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://${DE_ACCOUNT_PREFIX}-de-test-cassandra-1.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
echo "::add-mask::$CASSANDRA_TESTACCOUNT_TOKEN"
|
echo "::add-mask::$CASSANDRA_TESTACCOUNT_TOKEN"
|
||||||
echo CASSANDRA_TESTACCOUNT_TOKEN=$CASSANDRA_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
echo CASSANDRA_TESTACCOUNT_TOKEN=$CASSANDRA_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
MONGO_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo.documents.azure.com/.default" -o tsv --query accessToken)
|
MONGO_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://${DE_ACCOUNT_PREFIX}-de-test-mongo-1.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
echo "::add-mask::$MONGO_TESTACCOUNT_TOKEN"
|
echo "::add-mask::$MONGO_TESTACCOUNT_TOKEN"
|
||||||
echo MONGO_TESTACCOUNT_TOKEN=$MONGO_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
echo MONGO_TESTACCOUNT_TOKEN=$MONGO_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
MONGO32_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo32.documents.azure.com/.default" -o tsv --query accessToken)
|
MONGO32_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://${DE_ACCOUNT_PREFIX}-de-test-mongo32-1.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
echo "::add-mask::$MONGO32_TESTACCOUNT_TOKEN"
|
echo "::add-mask::$MONGO32_TESTACCOUNT_TOKEN"
|
||||||
echo MONGO32_TESTACCOUNT_TOKEN=$MONGO32_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
echo MONGO32_TESTACCOUNT_TOKEN=$MONGO32_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
MONGO_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo-readonly.documents.azure.com/.default" -o tsv --query accessToken)
|
MONGO_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://${DE_ACCOUNT_PREFIX}-de-test-mongo-readonly.documents.azure.com/.default" -o tsv --query accessToken)
|
||||||
echo "::add-mask::$MONGO_READONLY_TESTACCOUNT_TOKEN"
|
echo "::add-mask::$MONGO_READONLY_TESTACCOUNT_TOKEN"
|
||||||
echo MONGO_READONLY_TESTACCOUNT_TOKEN=$MONGO_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
echo MONGO_READONLY_TESTACCOUNT_TOKEN=$MONGO_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||||
- name: List test files for shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
|
- name: List test files for shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
|
||||||
@@ -303,9 +233,9 @@ jobs:
|
|||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
uses: Azure/login@v2
|
uses: Azure/login@v2
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }}
|
client-id: ${{ secrets.E2ETESTS_CLIENT_ID }}
|
||||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
tenant-id: ${{ secrets.E2ETESTS_TENANT_ID }}
|
||||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
subscription-id: ${{ secrets.E2ETESTS_SUBSCRIPTION_ID }}
|
||||||
- name: Upload shard blob-report to Azure Storage
|
- name: Upload shard blob-report to Azure Storage
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
env:
|
env:
|
||||||
@@ -313,7 +243,7 @@ jobs:
|
|||||||
SHARD: ${{ matrix.shardIndex }}
|
SHARD: ${{ matrix.shardIndex }}
|
||||||
run: |
|
run: |
|
||||||
az storage blob upload-batch \
|
az storage blob upload-batch \
|
||||||
--account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} \
|
--account-name ${{ secrets.E2ETESTS_STORAGEACCOUNT_NAME }} \
|
||||||
--auth-mode login \
|
--auth-mode login \
|
||||||
-d playwright-reports \
|
-d playwright-reports \
|
||||||
-s blob-report \
|
-s blob-report \
|
||||||
@@ -342,9 +272,9 @@ jobs:
|
|||||||
- name: "Az CLI login"
|
- name: "Az CLI login"
|
||||||
uses: Azure/login@v2
|
uses: Azure/login@v2
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
client-id: ${{ secrets.E2ETESTS_CLIENT_ID }}
|
||||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
tenant-id: ${{ secrets.E2ETESTS_TENANT_ID }}
|
||||||
subscription-id: ${{ secrets.PREVIEW_SUBSCRIPTION_ID }}
|
subscription-id: ${{ secrets.E2ETESTS_SUBSCRIPTION_ID }}
|
||||||
|
|
||||||
- name: Download all shard reports from Azure Storage
|
- name: Download all shard reports from Azure Storage
|
||||||
env:
|
env:
|
||||||
@@ -352,7 +282,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mkdir -p all-blob-reports
|
mkdir -p all-blob-reports
|
||||||
az storage blob download-batch \
|
az storage blob download-batch \
|
||||||
--account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} \
|
--account-name ${{ secrets.E2ETESTS_STORAGEACCOUNT_NAME }} \
|
||||||
--auth-mode login \
|
--auth-mode login \
|
||||||
-s playwright-reports \
|
-s playwright-reports \
|
||||||
--pattern "${KEY}/shards/*" \
|
--pattern "${KEY}/shards/*" \
|
||||||
@@ -375,7 +305,7 @@ jobs:
|
|||||||
KEY: ${{ github.run_id }}-${{ github.run_attempt }}
|
KEY: ${{ github.run_id }}-${{ github.run_attempt }}
|
||||||
run: |
|
run: |
|
||||||
az storage blob upload \
|
az storage blob upload \
|
||||||
--account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} \
|
--account-name ${{ secrets.E2ETESTS_STORAGEACCOUNT_NAME }} \
|
||||||
--auth-mode login \
|
--auth-mode login \
|
||||||
-c playwright-reports \
|
-c playwright-reports \
|
||||||
-n "${KEY}/report.zip" \
|
-n "${KEY}/report.zip" \
|
||||||
@@ -388,7 +318,7 @@ jobs:
|
|||||||
KEY: ${{ github.run_id }}-${{ github.run_attempt }}
|
KEY: ${{ github.run_id }}-${{ github.run_attempt }}
|
||||||
run: |
|
run: |
|
||||||
az storage blob delete-batch \
|
az storage blob delete-batch \
|
||||||
--account-name ${{ secrets.PREVIEW_STORAGE_ACCOUNT_NAME }} \
|
--account-name ${{ secrets.E2ETESTS_STORAGEACCOUNT_NAME }} \
|
||||||
--auth-mode login \
|
--auth-mode login \
|
||||||
-s playwright-reports \
|
-s playwright-reports \
|
||||||
--pattern "${KEY}/shards/*"
|
--pattern "${KEY}/shards/*"
|
||||||
@@ -398,7 +328,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
PR: ${{ github.event.pull_request.number }}
|
PR: ${{ github.event.pull_request.number }}
|
||||||
SA_ID: /subscriptions/${{ secrets.PREVIEW_SUBSCRIPTION_ID }}/resourceGroups/dataexplorer-preview/providers/Microsoft.Storage/storageAccounts/dataexplorerpreview
|
SA_ID: /subscriptions/${{ secrets.E2ETESTS_SUBSCRIPTION_ID }}/resourceGroups/${{ secrets.E2ETESTS_RESOURCEGROUP_NAME }}/providers/Microsoft.Storage/storageAccounts/${{ secrets.E2ETESTS_STORAGEACCOUNT_NAME }}
|
||||||
KEY: ${{ github.run_id }}-${{ github.run_attempt }}
|
KEY: ${{ github.run_id }}-${{ github.run_attempt }}
|
||||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -20,16 +20,16 @@ jobs:
|
|||||||
name: "Cleanup Test Database Accounts"
|
name: "Cleanup Test Database Accounts"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
AZURE_SUBSCRIPTION_ID: ${{ secrets.E2ETESTS_SUBSCRIPTION_ID }}
|
||||||
|
E2ETESTS_RESOURCEGROUP_NAME : ${{ secrets.E2ETESTS_RESOURCEGROUP_NAME }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: "Az CLI login"
|
- name: "Az CLI login"
|
||||||
uses: azure/login@v2
|
uses: azure/login@v2
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }}
|
client-id: ${{ secrets.E2ETESTS_CLIENT_ID }}
|
||||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
tenant-id: ${{ secrets.E2ETESTS_TENANT_ID }}
|
||||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
subscription-id: ${{ secrets.E2ETESTS_SUBSCRIPTION_ID }}
|
||||||
|
|
||||||
- name: Use Node.js 20.x
|
- name: Use Node.js 20.x
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ const fetch = require("node-fetch");
|
|||||||
|
|
||||||
const backendEndpoint = "https://cdb-ms-mpac-pbe.cosmos.azure.com";
|
const backendEndpoint = "https://cdb-ms-mpac-pbe.cosmos.azure.com";
|
||||||
const previewSiteEndpoint = "https://dataexplorer-preview.azurewebsites.net";
|
const previewSiteEndpoint = "https://dataexplorer-preview.azurewebsites.net";
|
||||||
const previewStorageWebsiteEndpoint = "https://dataexplorerpreview.z5.web.core.windows.net/";
|
const previewStorageWebsiteEndpoint = "_REPLACE_STORAGE_WEBSITE_ENDPOINT_";
|
||||||
const githubApiUrl = "https://api.github.com/repos/Azure/cosmos-explorer";
|
const githubApiUrl = "https://api.github.com/repos/Azure/cosmos-explorer";
|
||||||
const azurePortalMpacEndpoint = "https://ms.portal.azure.com/";
|
const azurePortalMpacEndpoint = "https://ms.portal.azure.com/";
|
||||||
|
|
||||||
|
|||||||
@@ -406,6 +406,7 @@ export enum TerminalKind {
|
|||||||
Cassandra = 2,
|
Cassandra = 2,
|
||||||
Postgres = 3,
|
Postgres = 3,
|
||||||
VCoreMongo = 4,
|
VCoreMongo = 4,
|
||||||
|
CosmosDB = 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DataExplorerInputsFrame {
|
export interface DataExplorerInputsFrame {
|
||||||
|
|||||||
@@ -179,7 +179,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
private shouldShowComputedPropertiesEditor: boolean;
|
private shouldShowComputedPropertiesEditor: boolean;
|
||||||
private shouldShowIndexingPolicyEditor: boolean;
|
private shouldShowIndexingPolicyEditor: boolean;
|
||||||
private shouldShowPartitionKeyEditor: boolean;
|
private shouldShowPartitionKeyEditor: boolean;
|
||||||
private isGlobalSecondaryIndex: boolean;
|
private isGlobalSecondaryIndexSource: boolean;
|
||||||
|
private isGlobalSecondaryIndexTarget: boolean;
|
||||||
private isVectorSearchEnabled: boolean;
|
private isVectorSearchEnabled: boolean;
|
||||||
private isFullTextSearchEnabled: boolean;
|
private isFullTextSearchEnabled: boolean;
|
||||||
private totalThroughputUsed: number;
|
private totalThroughputUsed: number;
|
||||||
@@ -197,8 +198,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL";
|
this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL";
|
||||||
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
|
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
|
||||||
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
|
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
|
||||||
this.isGlobalSecondaryIndex =
|
this.isGlobalSecondaryIndexSource = !!this.collection?.materializedViews();
|
||||||
!!this.collection?.materializedViewDefinition() || !!this.collection?.materializedViews();
|
this.isGlobalSecondaryIndexTarget = !!this.collection?.materializedViewDefinition();
|
||||||
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
||||||
this.isFullTextSearchEnabled = userContext.apiType === "SQL";
|
this.isFullTextSearchEnabled = userContext.apiType === "SQL";
|
||||||
|
|
||||||
@@ -1287,7 +1288,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
collection: this.collection,
|
collection: this.collection,
|
||||||
database: this.database,
|
database: this.database,
|
||||||
isFixedContainer: this.isFixedContainer,
|
isFixedContainer: this.isFixedContainer,
|
||||||
isGlobalSecondaryIndex: this.isGlobalSecondaryIndex,
|
isGlobalSecondaryIndexTarget: this.isGlobalSecondaryIndexTarget,
|
||||||
onThroughputChange: this.onThroughputChange,
|
onThroughputChange: this.onThroughputChange,
|
||||||
throughput: this.state.throughput,
|
throughput: this.state.throughput,
|
||||||
throughputBaseline: this.state.throughputBaseline,
|
throughputBaseline: this.state.throughputBaseline,
|
||||||
@@ -1356,7 +1357,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
isFullTextSearchEnabled: this.isFullTextSearchEnabled,
|
isFullTextSearchEnabled: this.isFullTextSearchEnabled,
|
||||||
shouldDiscardContainerPolicies: this.state.shouldDiscardContainerPolicies,
|
shouldDiscardContainerPolicies: this.state.shouldDiscardContainerPolicies,
|
||||||
resetShouldDiscardContainerPolicyChange: this.resetShouldDiscardContainerPolicies,
|
resetShouldDiscardContainerPolicyChange: this.resetShouldDiscardContainerPolicies,
|
||||||
isGlobalSecondaryIndex: this.isGlobalSecondaryIndex,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const indexingPolicyComponentProps: IndexingPolicyComponentProps = {
|
const indexingPolicyComponentProps: IndexingPolicyComponentProps = {
|
||||||
@@ -1509,7 +1509,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isGlobalSecondaryIndex) {
|
if (this.isGlobalSecondaryIndexTarget || this.isGlobalSecondaryIndexSource) {
|
||||||
tabs.push({
|
tabs.push({
|
||||||
tab: SettingsV2TabTypes.GlobalSecondaryIndexTab,
|
tab: SettingsV2TabTypes.GlobalSecondaryIndexTab,
|
||||||
content: <GlobalSecondaryIndexComponent {...globalSecondaryIndexComponentProps} />,
|
content: <GlobalSecondaryIndexComponent {...globalSecondaryIndexComponentProps} />,
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ export interface ContainerPolicyComponentProps {
|
|||||||
isFullTextSearchEnabled: boolean;
|
isFullTextSearchEnabled: boolean;
|
||||||
shouldDiscardContainerPolicies: boolean;
|
shouldDiscardContainerPolicies: boolean;
|
||||||
resetShouldDiscardContainerPolicyChange: () => void;
|
resetShouldDiscardContainerPolicyChange: () => void;
|
||||||
isGlobalSecondaryIndex?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> = ({
|
export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> = ({
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ describe("ScaleComponent", () => {
|
|||||||
collection: collection,
|
collection: collection,
|
||||||
database: undefined,
|
database: undefined,
|
||||||
isFixedContainer: false,
|
isFixedContainer: false,
|
||||||
isGlobalSecondaryIndex: false,
|
isGlobalSecondaryIndexTarget: false,
|
||||||
onThroughputChange: () => {
|
onThroughputChange: () => {
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export interface ScaleComponentProps {
|
|||||||
collection: ViewModels.Collection;
|
collection: ViewModels.Collection;
|
||||||
database: ViewModels.Database;
|
database: ViewModels.Database;
|
||||||
isFixedContainer: boolean;
|
isFixedContainer: boolean;
|
||||||
isGlobalSecondaryIndex: boolean;
|
isGlobalSecondaryIndexTarget: boolean;
|
||||||
onThroughputChange: (newThroughput: number) => void;
|
onThroughputChange: (newThroughput: number) => void;
|
||||||
throughput: number;
|
throughput: number;
|
||||||
throughputBaseline: number;
|
throughputBaseline: number;
|
||||||
@@ -147,7 +147,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
throughputError={this.props.throughputError}
|
throughputError={this.props.throughputError}
|
||||||
instantMaximumThroughput={this.offer?.instantMaximumThroughput}
|
instantMaximumThroughput={this.offer?.instantMaximumThroughput}
|
||||||
softAllowedMaximumThroughput={this.offer?.softAllowedMaximumThroughput}
|
softAllowedMaximumThroughput={this.offer?.softAllowedMaximumThroughput}
|
||||||
isGlobalSecondaryIndex={this.props.isGlobalSecondaryIndex}
|
isGlobalSecondaryIndexTarget={this.props.isGlobalSecondaryIndexTarget}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -44,7 +44,7 @@ describe("ThroughputInputAutoPilotV3Component", () => {
|
|||||||
},
|
},
|
||||||
instantMaximumThroughput: 5000,
|
instantMaximumThroughput: 5000,
|
||||||
softAllowedMaximumThroughput: 1000000,
|
softAllowedMaximumThroughput: 1000000,
|
||||||
isGlobalSecondaryIndex: false,
|
isGlobalSecondaryIndexTarget: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
it("throughput input visible", () => {
|
it("throughput input visible", () => {
|
||||||
|
|||||||
+2
-2
@@ -81,7 +81,7 @@ export interface ThroughputInputAutoPilotV3Props {
|
|||||||
throughputError?: string;
|
throughputError?: string;
|
||||||
instantMaximumThroughput: number;
|
instantMaximumThroughput: number;
|
||||||
softAllowedMaximumThroughput: number;
|
softAllowedMaximumThroughput: number;
|
||||||
isGlobalSecondaryIndex: boolean;
|
isGlobalSecondaryIndexTarget: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ThroughputInputAutoPilotV3State {
|
interface ThroughputInputAutoPilotV3State {
|
||||||
@@ -427,7 +427,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
toolTipElement={getToolTipContainer(this.props.infoBubbleText)}
|
toolTipElement={getToolTipContainer(this.props.infoBubbleText)}
|
||||||
/>
|
/>
|
||||||
</Label>
|
</Label>
|
||||||
{!this.props.isGlobalSecondaryIndex && (
|
{!this.props.isGlobalSecondaryIndexTarget && (
|
||||||
<>
|
<>
|
||||||
{this.overrideWithProvisionedThroughputSettings() && (
|
{this.overrideWithProvisionedThroughputSettings() && (
|
||||||
<MessageBar
|
<MessageBar
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
}
|
}
|
||||||
isAutoPilotSelected={false}
|
isAutoPilotSelected={false}
|
||||||
isFixedContainer={false}
|
isFixedContainer={false}
|
||||||
isGlobalSecondaryIndex={true}
|
isGlobalSecondaryIndexTarget={true}
|
||||||
onAutoPilotSelected={[Function]}
|
onAutoPilotSelected={[Function]}
|
||||||
onMaxAutoPilotThroughputChange={[Function]}
|
onMaxAutoPilotThroughputChange={[Function]}
|
||||||
onScaleDiscardableChange={[Function]}
|
onScaleDiscardableChange={[Function]}
|
||||||
@@ -350,7 +350,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
fullTextPolicy={{}}
|
fullTextPolicy={{}}
|
||||||
fullTextPolicyBaseline={{}}
|
fullTextPolicyBaseline={{}}
|
||||||
isFullTextSearchEnabled={true}
|
isFullTextSearchEnabled={true}
|
||||||
isGlobalSecondaryIndex={true}
|
|
||||||
isVectorSearchEnabled={false}
|
isVectorSearchEnabled={false}
|
||||||
onFullTextPolicyChange={[Function]}
|
onFullTextPolicyChange={[Function]}
|
||||||
onFullTextPolicyDirtyChange={[Function]}
|
onFullTextPolicyDirtyChange={[Function]}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export interface ThroughputInputProps {
|
|||||||
isFreeTier: boolean;
|
isFreeTier: boolean;
|
||||||
showFreeTierExceedThroughputTooltip: boolean;
|
showFreeTierExceedThroughputTooltip: boolean;
|
||||||
isQuickstart?: boolean;
|
isQuickstart?: boolean;
|
||||||
isGlobalSecondaryIndex?: boolean;
|
isGlobalSecondaryIndexTarget?: boolean;
|
||||||
setThroughputValue: (throughput: number) => void;
|
setThroughputValue: (throughput: number) => void;
|
||||||
setIsAutoscale: (isAutoscale: boolean) => void;
|
setIsAutoscale: (isAutoscale: boolean) => void;
|
||||||
setIsThroughputCapExceeded: (isThroughputCapExceeded: boolean) => void;
|
setIsThroughputCapExceeded: (isThroughputCapExceeded: boolean) => void;
|
||||||
@@ -32,7 +32,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
isFreeTier,
|
isFreeTier,
|
||||||
showFreeTierExceedThroughputTooltip,
|
showFreeTierExceedThroughputTooltip,
|
||||||
isQuickstart,
|
isQuickstart,
|
||||||
isGlobalSecondaryIndex,
|
isGlobalSecondaryIndexTarget,
|
||||||
setThroughputValue,
|
setThroughputValue,
|
||||||
setIsAutoscale,
|
setIsAutoscale,
|
||||||
setIsThroughputCapExceeded,
|
setIsThroughputCapExceeded,
|
||||||
@@ -202,7 +202,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
</Text>
|
</Text>
|
||||||
<InfoTooltip>{PricingUtils.getRuToolTipText()}</InfoTooltip>
|
<InfoTooltip>{PricingUtils.getRuToolTipText()}</InfoTooltip>
|
||||||
</Stack>
|
</Stack>
|
||||||
{!isGlobalSecondaryIndex && (
|
{!isGlobalSecondaryIndexTarget && (
|
||||||
<Stack horizontal verticalAlign="center">
|
<Stack horizontal verticalAlign="center">
|
||||||
<div role="radiogroup">
|
<div role="radiogroup">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export interface IVectorEmbeddingPoliciesComponentProps {
|
|||||||
vectorIndexes?: VectorIndex[];
|
vectorIndexes?: VectorIndex[];
|
||||||
discardChanges?: boolean;
|
discardChanges?: boolean;
|
||||||
onChangesDiscarded?: () => void;
|
onChangesDiscarded?: () => void;
|
||||||
isGlobalSecondaryIndex?: boolean;
|
isGlobalSecondaryIndexTarget?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VectorEmbeddingPolicyData {
|
export interface VectorEmbeddingPolicyData {
|
||||||
@@ -95,7 +95,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
|||||||
onVectorEmbeddingChange,
|
onVectorEmbeddingChange,
|
||||||
discardChanges,
|
discardChanges,
|
||||||
onChangesDiscarded,
|
onChangesDiscarded,
|
||||||
isGlobalSecondaryIndex,
|
isGlobalSecondaryIndexTarget,
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const isExistingPolicy = (policy: VectorEmbeddingPolicyData): boolean => {
|
const isExistingPolicy = (policy: VectorEmbeddingPolicyData): boolean => {
|
||||||
if (!vectorEmbeddingsBaseline || vectorEmbeddingsBaseline.length === 0) {
|
if (!vectorEmbeddingsBaseline || vectorEmbeddingsBaseline.length === 0) {
|
||||||
@@ -166,7 +166,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
|||||||
indexType: matchingType || "none",
|
indexType: matchingType || "none",
|
||||||
indexingSearchListSize: matchingIndex?.indexingSearchListSize || undefined,
|
indexingSearchListSize: matchingIndex?.indexingSearchListSize || undefined,
|
||||||
quantizationByteSize: matchingIndex?.quantizationByteSize || undefined,
|
quantizationByteSize: matchingIndex?.quantizationByteSize || undefined,
|
||||||
quantizerType: supportsQuantizer ? matchingIndex?.quantizerType || "product" : undefined,
|
quantizerType: supportsQuantizer ? matchingIndex?.quantizerType || "spherical" : undefined,
|
||||||
vectorIndexShardKey: matchingIndex?.vectorIndexShardKey || undefined,
|
vectorIndexShardKey: matchingIndex?.vectorIndexShardKey || undefined,
|
||||||
pathError: onVectorEmbeddingPathError(embedding.path),
|
pathError: onVectorEmbeddingPathError(embedding.path),
|
||||||
dimensionsError: onVectorEmbeddingDimensionError(embedding.dimensions, matchingIndex?.type || "none"),
|
dimensionsError: onVectorEmbeddingDimensionError(embedding.dimensions, matchingIndex?.type || "none"),
|
||||||
@@ -256,7 +256,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
|||||||
vectorEmbedding.indexingSearchListSize = undefined;
|
vectorEmbedding.indexingSearchListSize = undefined;
|
||||||
}
|
}
|
||||||
if (supportsQuantization(vectorEmbedding.indexType)) {
|
if (supportsQuantization(vectorEmbedding.indexType)) {
|
||||||
vectorEmbedding.quantizerType = vectorEmbedding.quantizerType || "product";
|
vectorEmbedding.quantizerType = vectorEmbedding.quantizerType || "spherical";
|
||||||
} else {
|
} else {
|
||||||
vectorEmbedding.quantizerType = undefined;
|
vectorEmbedding.quantizerType = undefined;
|
||||||
}
|
}
|
||||||
@@ -327,7 +327,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getQuantizationByteSizeTooltipContent = (): string => {
|
const getQuantizationByteSizeTooltipContent = (): string => {
|
||||||
const containerName = isGlobalSecondaryIndex
|
const containerName = isGlobalSecondaryIndexTarget
|
||||||
? t(Keys.controls.vectorEmbeddingPolicies.quantizationByteSizeTooltipGlobalSecondaryIndexName)
|
? t(Keys.controls.vectorEmbeddingPolicies.quantizationByteSizeTooltipGlobalSecondaryIndexName)
|
||||||
: t(Keys.controls.vectorEmbeddingPolicies.quantizationByteSizeTooltipContainerName);
|
: t(Keys.controls.vectorEmbeddingPolicies.quantizationByteSizeTooltipContainerName);
|
||||||
return t(Keys.controls.vectorEmbeddingPolicies.quantizationByteSizeTooltip, { containerName });
|
return t(Keys.controls.vectorEmbeddingPolicies.quantizationByteSizeTooltip, { containerName });
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { IDropdownOption } from "@fluentui/react";
|
import { IDropdownOption } from "@fluentui/react";
|
||||||
import { VectorIndex } from "Contracts/DataModels";
|
import { VectorIndex } from "Contracts/DataModels";
|
||||||
import { Keys, t } from "Localization";
|
|
||||||
|
|
||||||
const dataTypes = ["float32", "uint8", "int8", "float16"];
|
const dataTypes = ["float32", "uint8", "int8", "float16"];
|
||||||
const distanceFunctions = ["euclidean", "cosine", "dotproduct"];
|
const distanceFunctions = ["euclidean", "cosine", "dotproduct"];
|
||||||
@@ -10,8 +9,8 @@ export const getDataTypeOptions = (): IDropdownOption[] => createDropdownOptions
|
|||||||
export const getDistanceFunctionOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(distanceFunctions);
|
export const getDistanceFunctionOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(distanceFunctions);
|
||||||
export const getIndexTypeOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(indexTypes);
|
export const getIndexTypeOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(indexTypes);
|
||||||
export const getQuantizerTypeOptions = (): IDropdownOption[] => [
|
export const getQuantizerTypeOptions = (): IDropdownOption[] => [
|
||||||
|
{ key: "spherical", text: "Spherical" },
|
||||||
{ key: "product", text: "Product" },
|
{ key: "product", text: "Product" },
|
||||||
{ key: "spherical", text: `Spherical (${t(Keys.common.preview)})` },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const supportsQuantization = (indexType: VectorIndex["type"] | "none" | undefined): boolean =>
|
export const supportsQuantization = (indexType: VectorIndex["type"] | "none" | undefined): boolean =>
|
||||||
|
|||||||
@@ -666,6 +666,10 @@ export default class Explorer {
|
|||||||
title = "Mongo Shell";
|
title = "Mongo Shell";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case ViewModels.TerminalKind.CosmosDB:
|
||||||
|
title = "Cosmos DB Shell";
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error("Terminal kind: ${kind} not supported");
|
throw new Error("Terminal kind: ${kind} not supported");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ import SynapseIcon from "../../../../images/synapse-link.svg";
|
|||||||
import VSCodeIcon from "../../../../images/vscode.svg";
|
import VSCodeIcon from "../../../../images/vscode.svg";
|
||||||
import { AuthType } from "../../../AuthType";
|
import { AuthType } from "../../../AuthType";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
import { Platform, configContext } from "../../../ConfigContext";
|
import { configContext, Platform } from "../../../ConfigContext";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import {
|
import {
|
||||||
userContext,
|
|
||||||
isVCoreMongoNativeAuthDisabled,
|
isVCoreMongoNativeAuthDisabled,
|
||||||
|
userContext,
|
||||||
VCoreMongoNativeAuthDisabledMessage,
|
VCoreMongoNativeAuthDisabledMessage,
|
||||||
VCoreMongoNativeAuthLearnMoreUrl,
|
VCoreMongoNativeAuthLearnMoreUrl,
|
||||||
} from "../../../UserContext";
|
} from "../../../UserContext";
|
||||||
@@ -81,6 +81,15 @@ export function createStaticCommandBarButtons(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
userContext.apiType === "SQL" &&
|
||||||
|
userContext.features.enableCloudShell &&
|
||||||
|
userContext.features.enableCosmosDBShell
|
||||||
|
) {
|
||||||
|
addDivider();
|
||||||
|
buttons.push(createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.CosmosDB));
|
||||||
|
}
|
||||||
|
|
||||||
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
|
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
|
||||||
const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
||||||
|
|
||||||
@@ -498,6 +507,8 @@ function createOpenTerminalButtonByKind(
|
|||||||
return "PSQL";
|
return "PSQL";
|
||||||
case ViewModels.TerminalKind.VCoreMongo:
|
case ViewModels.TerminalKind.VCoreMongo:
|
||||||
return "MongoDB (DocumentDB)";
|
return "MongoDB (DocumentDB)";
|
||||||
|
case ViewModels.TerminalKind.CosmosDB:
|
||||||
|
return "Cosmos DB";
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -507,7 +518,9 @@ function createOpenTerminalButtonByKind(
|
|||||||
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
|
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
|
||||||
const isNativeAuthDisabled = terminalKind === ViewModels.TerminalKind.VCoreMongo && isVCoreMongoNativeAuthDisabled();
|
const isNativeAuthDisabled = terminalKind === ViewModels.TerminalKind.VCoreMongo && isVCoreMongoNativeAuthDisabled();
|
||||||
const disableButton =
|
const disableButton =
|
||||||
(!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled) ||
|
(!useNotebook.getState().isNotebooksEnabledForAccount &&
|
||||||
|
!useNotebook.getState().isNotebookEnabled &&
|
||||||
|
!userContext.features.enableCloudShell) ||
|
||||||
isNativeAuthDisabled;
|
isNativeAuthDisabled;
|
||||||
return {
|
return {
|
||||||
iconSrc: HostedTerminalIcon,
|
iconSrc: HostedTerminalIcon,
|
||||||
|
|||||||
@@ -88,12 +88,12 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
|||||||
});
|
});
|
||||||
|
|
||||||
database.collections().forEach((collection: Collection) => {
|
database.collections().forEach((collection: Collection) => {
|
||||||
const isGlobalSecondaryIndex: boolean = !!collection.materializedViewDefinition();
|
const isGlobalSecondaryIndexTarget: boolean = !!collection.materializedViewDefinition();
|
||||||
sourceContainerOptions.push({
|
sourceContainerOptions.push({
|
||||||
key: collection.rid,
|
key: collection.rid,
|
||||||
text: collection.id(),
|
text: collection.id(),
|
||||||
disabled: isGlobalSecondaryIndex,
|
disabled: isGlobalSecondaryIndexTarget,
|
||||||
...(isGlobalSecondaryIndex && {
|
...(isGlobalSecondaryIndexTarget && {
|
||||||
title: "This is a global secondary index.",
|
title: "This is a global secondary index.",
|
||||||
}),
|
}),
|
||||||
data: collection,
|
data: collection,
|
||||||
@@ -382,7 +382,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
|||||||
setVectorIndexingPolicy,
|
setVectorIndexingPolicy,
|
||||||
vectorPolicyValidated,
|
vectorPolicyValidated,
|
||||||
setVectorPolicyValidated,
|
setVectorPolicyValidated,
|
||||||
isGlobalSecondaryIndex: true,
|
isGlobalSecondaryIndexTarget: true,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export const ThroughputComponent = (props: ThroughputComponentProps): JSX.Elemen
|
|||||||
isSharded={true}
|
isSharded={true}
|
||||||
isFreeTier={isFreeTierAccount()}
|
isFreeTier={isFreeTierAccount()}
|
||||||
isQuickstart={false}
|
isQuickstart={false}
|
||||||
isGlobalSecondaryIndex={true}
|
isGlobalSecondaryIndexTarget={true}
|
||||||
setThroughputValue={(throughput: number) => {
|
setThroughputValue={(throughput: number) => {
|
||||||
globalSecondaryIndexThroughputOnChange(throughput);
|
globalSecondaryIndexThroughputOnChange(throughput);
|
||||||
}}
|
}}
|
||||||
|
|||||||
+3
-3
@@ -14,7 +14,7 @@ export interface VectorSearchComponentProps {
|
|||||||
vectorIndexingPolicy: VectorIndex[];
|
vectorIndexingPolicy: VectorIndex[];
|
||||||
setVectorIndexingPolicy: React.Dispatch<React.SetStateAction<VectorIndex[]>>;
|
setVectorIndexingPolicy: React.Dispatch<React.SetStateAction<VectorIndex[]>>;
|
||||||
setVectorPolicyValidated: React.Dispatch<React.SetStateAction<boolean>>;
|
setVectorPolicyValidated: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
isGlobalSecondaryIndex?: boolean;
|
isGlobalSecondaryIndexTarget?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VectorSearchComponent = (props: VectorSearchComponentProps): JSX.Element => {
|
export const VectorSearchComponent = (props: VectorSearchComponentProps): JSX.Element => {
|
||||||
@@ -24,7 +24,7 @@ export const VectorSearchComponent = (props: VectorSearchComponentProps): JSX.El
|
|||||||
vectorIndexingPolicy,
|
vectorIndexingPolicy,
|
||||||
setVectorIndexingPolicy,
|
setVectorIndexingPolicy,
|
||||||
setVectorPolicyValidated,
|
setVectorPolicyValidated,
|
||||||
isGlobalSecondaryIndex,
|
isGlobalSecondaryIndexTarget,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -52,7 +52,7 @@ export const VectorSearchComponent = (props: VectorSearchComponentProps): JSX.El
|
|||||||
setVectorIndexingPolicy(vectorIndexingPolicy);
|
setVectorIndexingPolicy(vectorIndexingPolicy);
|
||||||
setVectorPolicyValidated(vectorPolicyValidated);
|
setVectorPolicyValidated(vectorPolicyValidated);
|
||||||
}}
|
}}
|
||||||
isGlobalSecondaryIndex={isGlobalSecondaryIndex}
|
isGlobalSecondaryIndexTarget={isGlobalSecondaryIndexTarget}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ describe("CloudShellClient", () => {
|
|||||||
vnetSettings: {},
|
vnetSettings: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
timeoutMs: 30000,
|
||||||
});
|
});
|
||||||
expect(result).toEqual(mockResponse);
|
expect(result).toEqual(mockResponse);
|
||||||
});
|
});
|
||||||
@@ -154,6 +155,7 @@ describe("CloudShellClient", () => {
|
|||||||
vnetSettings: mockVNetSettings,
|
vnetSettings: mockVNetSettings,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
timeoutMs: 30000,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -212,6 +214,7 @@ describe("CloudShellClient", () => {
|
|||||||
path: "/subscriptions/sub-id/providers/Microsoft.CloudShell/register",
|
path: "/subscriptions/sub-id/providers/Microsoft.CloudShell/register",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
apiVersion: "2022-12-01",
|
apiVersion: "2022-12-01",
|
||||||
|
timeoutMs: 30000,
|
||||||
});
|
});
|
||||||
expect(result).toEqual(mockResponse);
|
expect(result).toEqual(mockResponse);
|
||||||
});
|
});
|
||||||
@@ -227,6 +230,7 @@ describe("CloudShellClient", () => {
|
|||||||
path: "/subscriptions/sub-id/providers/Microsoft.CloudShell/register",
|
path: "/subscriptions/sub-id/providers/Microsoft.CloudShell/register",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
apiVersion: "2022-12-01",
|
apiVersion: "2022-12-01",
|
||||||
|
timeoutMs: 30000,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -251,6 +255,7 @@ describe("CloudShellClient", () => {
|
|||||||
osType: OsType.Linux,
|
osType: OsType.Linux,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
timeoutMs: 30000,
|
||||||
});
|
});
|
||||||
expect(result).toEqual(mockResponse);
|
expect(result).toEqual(mockResponse);
|
||||||
});
|
});
|
||||||
@@ -274,6 +279,7 @@ describe("CloudShellClient", () => {
|
|||||||
osType: OsType.Linux,
|
osType: OsType.Linux,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
timeoutMs: 30000,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ import {
|
|||||||
} from "../Models/DataModels";
|
} from "../Models/DataModels";
|
||||||
import { getLocale } from "../Utils/CommonUtils";
|
import { getLocale } from "../Utils/CommonUtils";
|
||||||
|
|
||||||
|
// armRequest defaults to a 5s timeout with no retry for PUT/POST. Cloud Shell provisioning
|
||||||
|
// (registering the provider, applying settings, and provisioning the console itself) can
|
||||||
|
// legitimately take much longer than that on first use or when the console region differs
|
||||||
|
// from the user's usual region, so a much more generous timeout is used for these calls to
|
||||||
|
// avoid a spurious AbortError ("User aborted query.") aborting the whole session start.
|
||||||
|
const CLOUDSHELL_ARM_TIMEOUT_MS = 30000;
|
||||||
|
|
||||||
export const getUserSettings = async (): Promise<CloudShellSettings> => {
|
export const getUserSettings = async (): Promise<CloudShellSettings> => {
|
||||||
return await armRequest<CloudShellSettings>({
|
return await armRequest<CloudShellSettings>({
|
||||||
host: configContext.ARM_ENDPOINT,
|
host: configContext.ARM_ENDPOINT,
|
||||||
@@ -51,6 +58,7 @@ export const putEphemeralUserSettings = async (
|
|||||||
method: "PUT",
|
method: "PUT",
|
||||||
apiVersion: "2023-02-01-preview",
|
apiVersion: "2023-02-01-preview",
|
||||||
body: ephemeralSettings,
|
body: ephemeralSettings,
|
||||||
|
timeoutMs: CLOUDSHELL_ARM_TIMEOUT_MS,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -69,6 +77,7 @@ export const registerCloudShellProvider = async (subscriptionId: string) => {
|
|||||||
path: `/subscriptions/${subscriptionId}/providers/Microsoft.CloudShell/register`,
|
path: `/subscriptions/${subscriptionId}/providers/Microsoft.CloudShell/register`,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
apiVersion: "2022-12-01",
|
apiVersion: "2022-12-01",
|
||||||
|
timeoutMs: CLOUDSHELL_ARM_TIMEOUT_MS,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -88,6 +97,7 @@ export const provisionConsole = async (consoleLocation: string): Promise<Provisi
|
|||||||
"x-ms-console-preferred-location": consoleLocation,
|
"x-ms-console-preferred-location": consoleLocation,
|
||||||
},
|
},
|
||||||
body: data,
|
body: data,
|
||||||
|
timeoutMs: CLOUDSHELL_ARM_TIMEOUT_MS,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { CosmosDBShellHandler } from "./CosmosDBShellHandler";
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
jest.mock("../../../../UserContext", () => ({
|
||||||
|
userContext: {
|
||||||
|
databaseAccount: {
|
||||||
|
properties: {
|
||||||
|
documentEndpoint: "https://test-account.documents.azure.com:443/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("CosmosDBShellHandler", () => {
|
||||||
|
const mockKey = "testKey";
|
||||||
|
const endpoint = "https://test-account.documents.azure.com:443/";
|
||||||
|
const tokenConnectionCommand = `export COSMOSDB_SHELL_TOKEN='aadToken123'; cosmosdbshell --connect '${endpoint}' --connect-mode gateway --verbose`;
|
||||||
|
const keyConnectionCommand = `export COSMOSDB_SHELL_ACCOUNT_KEY='${mockKey}'; cosmosdbshell --connect '${endpoint}' --connect-mode gateway --verbose`;
|
||||||
|
let cosmosDBShellHandler: CosmosDBShellHandler;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cosmosDBShellHandler = new CosmosDBShellHandler({ kind: "key", value: mockKey });
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
jest.resetModules();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Positive Tests", () => {
|
||||||
|
it("should return correct shell name", () => {
|
||||||
|
expect(cosmosDBShellHandler.getShellName()).toBe("Cosmos DB");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should install the CosmosDBShell tool in setup commands", () => {
|
||||||
|
const commands = cosmosDBShellHandler.getSetUpCommands();
|
||||||
|
|
||||||
|
expect(Array.isArray(commands)).toBe(true);
|
||||||
|
expect(commands.some((c) => c.includes("dotnet tool install --global CosmosDBShell --prerelease"))).toBe(true);
|
||||||
|
expect(commands.some((c) => c.includes("dotnet tool update --global CosmosDBShell --prerelease"))).toBe(true);
|
||||||
|
expect(commands.some((c) => c.includes("$HOME/.dotnet/tools"))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should bootstrap a .NET SDK 10 when the tool and SDK are missing", () => {
|
||||||
|
const commands = cosmosDBShellHandler.getSetUpCommands();
|
||||||
|
|
||||||
|
expect(commands.some((c) => c.includes("dotnet-install.sh") && c.includes("--channel 10.0"))).toBe(true);
|
||||||
|
expect(commands.some((c) => c === "export DOTNET_ROOT=$HOME/.dotnet")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not export any credential env var in setup commands for a key credential", () => {
|
||||||
|
const commands = cosmosDBShellHandler.getSetUpCommands();
|
||||||
|
|
||||||
|
expect(commands.some((c) => c.includes("COSMOSDB_SHELL_"))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not export any credential env var in setup commands for a token credential", () => {
|
||||||
|
const handler = new CosmosDBShellHandler({ kind: "token", value: "aadToken123" });
|
||||||
|
const commands = handler.getSetUpCommands();
|
||||||
|
|
||||||
|
expect(commands.some((c) => c.includes("COSMOSDB_SHELL_"))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should export the account key and connect to the bare endpoint on the same command line", () => {
|
||||||
|
expect(cosmosDBShellHandler.getConnectionCommand()).toBe(keyConnectionCommand);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should export the token and connect to the bare endpoint on the same command line", () => {
|
||||||
|
const handler = new CosmosDBShellHandler({ kind: "token", value: "aadToken123" });
|
||||||
|
|
||||||
|
expect(handler.getConnectionCommand()).toBe(tokenConnectionCommand);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should never pass an interactive or ambient credential flag", () => {
|
||||||
|
const handler = new CosmosDBShellHandler({ kind: "token", value: "aadToken123" });
|
||||||
|
const connectionCommand = handler.getConnectionCommand();
|
||||||
|
|
||||||
|
expect(connectionCommand).not.toContain("--connect-tenant");
|
||||||
|
expect(connectionCommand).not.toContain("--connect-hint");
|
||||||
|
expect(connectionCommand).not.toContain("--connect-authority-host");
|
||||||
|
expect(connectionCommand).not.toContain("--connect-azure-cli");
|
||||||
|
expect(connectionCommand).not.toContain("--connect-managed-identity");
|
||||||
|
expect(connectionCommand).not.toContain("--connect-vscode-credential");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array for terminal suppressed data", () => {
|
||||||
|
expect(cosmosDBShellHandler.getTerminalSuppressedData()).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Negative Tests", () => {
|
||||||
|
it("should not export a credential env var when no credential was resolved", () => {
|
||||||
|
const handler = new CosmosDBShellHandler(undefined);
|
||||||
|
const commands = handler.getSetUpCommands();
|
||||||
|
|
||||||
|
expect(commands.some((c) => c.includes("COSMOSDB_SHELL_"))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not launch the shell when no credential was resolved", () => {
|
||||||
|
const handler = new CosmosDBShellHandler(undefined);
|
||||||
|
const connectionCommand = handler.getConnectionCommand();
|
||||||
|
|
||||||
|
expect(connectionCommand).not.toContain("cosmosdbshell --connect");
|
||||||
|
expect(connectionCommand).toContain("Unable to acquire a Cosmos DB credential");
|
||||||
|
expect(connectionCommand).toContain("Login for Entra ID");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should echo the specific reason when one is provided", () => {
|
||||||
|
const handler = new CosmosDBShellHandler(undefined, "listing the account keys failed (Forbidden)");
|
||||||
|
const connectionCommand = handler.getConnectionCommand();
|
||||||
|
|
||||||
|
expect(connectionCommand).toContain("Unable to acquire a Cosmos DB credential");
|
||||||
|
expect(connectionCommand).toContain("listing the account keys failed (Forbidden)");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { userContext } from "../../../../UserContext";
|
||||||
|
import { AbstractShellHandler } from "./AbstractShellHandler";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A credential resolved by Data Explorer and handed to the Cosmos DB Shell.
|
||||||
|
*
|
||||||
|
* Both kinds are delivered the same way the Mongo shell delivers its credential: exported
|
||||||
|
* as an environment variable on the same command line as the `cosmosdbshell` invocation
|
||||||
|
* (`export VAR='...'; cosmosdbshell --connect <endpoint>`), immediately before the tool
|
||||||
|
* reads it, rather than as a separate setup step or embedded in a connection string.
|
||||||
|
*
|
||||||
|
* - `token` is an Entra ID (AAD) bearer token scoped to the account's data plane, exported
|
||||||
|
* as COSMOSDB_SHELL_TOKEN.
|
||||||
|
* - `key` is an account master (or read-only) key, exported as COSMOSDB_SHELL_ACCOUNT_KEY.
|
||||||
|
*
|
||||||
|
* The kind is tracked explicitly so the correct environment variable is always used.
|
||||||
|
*/
|
||||||
|
export interface CosmosDBShellCredential {
|
||||||
|
kind: "token" | "key";
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shell handler for the Azure Cosmos DB Shell (https://github.com/Azure/CosmosDBShell),
|
||||||
|
* a .NET global tool that targets the Cosmos DB NoSQL (SQL Core) API.
|
||||||
|
*/
|
||||||
|
export class CosmosDBShellHandler extends AbstractShellHandler {
|
||||||
|
private _endpoint: string | undefined;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private credential: CosmosDBShellCredential | undefined,
|
||||||
|
private unavailableReason?: string,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this._endpoint = userContext?.databaseAccount?.properties?.documentEndpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getShellName(): string {
|
||||||
|
return "Cosmos DB";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup commands for the Cosmos DB Shell:
|
||||||
|
*
|
||||||
|
* 1. Put the private .NET install dir and global tools dir on PATH (and set
|
||||||
|
* DOTNET_ROOT) so both the `dotnet` host and installed tool resolve.
|
||||||
|
* 2. Bootstrap a private .NET SDK 10 into $HOME/.dotnet when neither the
|
||||||
|
* cosmosdbshell tool nor a suitable SDK is already available. The
|
||||||
|
* CosmosDBShell global tool targets net10.0, so `dotnet tool install`
|
||||||
|
* requires the .NET SDK 10.0+, which the Azure Cloud Shell host does not
|
||||||
|
* ship by default.
|
||||||
|
* 3. Install the CosmosDBShell global tool if it is not already available, or
|
||||||
|
* update it to the latest release when it is (so an older cached install in
|
||||||
|
* the persistent Cloud Shell $HOME picks up newer connect options).
|
||||||
|
* 4. Persist the PATH/DOTNET_ROOT changes for future sessions.
|
||||||
|
*
|
||||||
|
* The credential itself is not exported here: it travels on the same command line as
|
||||||
|
* the `cosmosdbshell` invocation (see getConnectionCommand), mirroring how the Mongo
|
||||||
|
* shell handler builds its connection command.
|
||||||
|
*
|
||||||
|
* Installation steps run conditionally only if cosmosdbshell is not already
|
||||||
|
* present in the environment.
|
||||||
|
*/
|
||||||
|
public getSetUpCommands(): string[] {
|
||||||
|
return [
|
||||||
|
"export DOTNET_ROOT=$HOME/.dotnet",
|
||||||
|
"export PATH=$HOME/.dotnet:$HOME/.dotnet/tools:$PATH",
|
||||||
|
"if ! command -v cosmosdbshell &> /dev/null; then echo '⚠️ cosmosdbshell not found. Installing .NET SDK 10 and CosmosDBShell...'; fi",
|
||||||
|
"if ! command -v cosmosdbshell &> /dev/null && ! dotnet --list-sdks 2>/dev/null | grep -q '^10\\.'; then curl -sSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel 10.0 --install-dir $HOME/.dotnet; fi",
|
||||||
|
"if ! command -v cosmosdbshell &> /dev/null; then dotnet tool install --global CosmosDBShell --prerelease; else dotnet tool update --global CosmosDBShell --prerelease; fi",
|
||||||
|
"grep -qxF 'export DOTNET_ROOT=$HOME/.dotnet' ~/.bashrc || echo 'export DOTNET_ROOT=$HOME/.dotnet' >> ~/.bashrc",
|
||||||
|
"grep -qxF 'export PATH=$HOME/.dotnet:$HOME/.dotnet/tools:$PATH' ~/.bashrc || echo 'export PATH=$HOME/.dotnet:$HOME/.dotnet/tools:$PATH' >> ~/.bashrc",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getKeyConnectionCommand(key: string): string {
|
||||||
|
// Export the key immediately before invoking the tool, on the same command line, so it
|
||||||
|
// never appears as a --connect flag value or lands in a separate setup step. The tool
|
||||||
|
// reads the account key from COSMOSDB_SHELL_ACCOUNT_KEY and connects to the bare endpoint.
|
||||||
|
return `export COSMOSDB_SHELL_ACCOUNT_KEY='${key}'; cosmosdbshell --connect '${this._endpoint}' --connect-mode gateway --verbose`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getTokenConnectionCommand(token: string): string {
|
||||||
|
// Same pattern for the Entra ID token: exported right before the invocation so it never
|
||||||
|
// appears in argv/ps, then the tool reads it from COSMOSDB_SHELL_TOKEN.
|
||||||
|
return `export COSMOSDB_SHELL_TOKEN='${token}'; cosmosdbshell --connect '${this._endpoint}' --connect-mode gateway --verbose`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getConnectionCommand(): string {
|
||||||
|
if (!this._endpoint) {
|
||||||
|
return `echo '${this.getShellName()} endpoint not found.'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.credential) {
|
||||||
|
// Never let the tool continue without a credential. With no account key and no
|
||||||
|
// COSMOSDB_SHELL_TOKEN exported, its credential chain would fall through to
|
||||||
|
// DefaultAzureCredential, which in Azure Cloud Shell tries the managed identity
|
||||||
|
// first and fails with "AudienceNotSupported" (the Cloud Shell MSI cannot mint a
|
||||||
|
// token for the *.documents.azure.com audience). The interactive browser and
|
||||||
|
// device-code flows are not usable from this embedded terminal either, so fail
|
||||||
|
// fast with actionable guidance instead.
|
||||||
|
//
|
||||||
|
// There is no way to inspect the browser dev tools console from inside this remote
|
||||||
|
// terminal, so the specific reason (when known) is echoed here too.
|
||||||
|
const reasonSuffix = this.unavailableReason ? ` — ${this.unavailableReason}` : "";
|
||||||
|
return `echo 'Unable to acquire a ${this.getShellName()} credential${reasonSuffix}. Use "Login for Entra ID" in the Data Explorer toolbar, verify you have Cosmos DB data-plane RBAC access to this account, then reopen the shell.'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force gateway (HTTPS/443) connection mode. The shell otherwise defaults to
|
||||||
|
// direct (TCP) mode for real accounts, and Azure Cloud Shell blocks the
|
||||||
|
// direct-mode TCP ports, causing the connection to fail. `--verbose` surfaces
|
||||||
|
// the full exception details when a connection attempt fails.
|
||||||
|
//
|
||||||
|
// Surface which credential kind Explorer resolved in the browser console. A wrong
|
||||||
|
// guess here (e.g. a token when the account actually needs a key) otherwise fails
|
||||||
|
// silently inside the remote shell with no client-side signal to debug against.
|
||||||
|
console.warn(`CloudShell: connecting to Cosmos DB shell with a "${this.credential.kind}" credential`, {
|
||||||
|
endpoint: this._endpoint,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.credential.kind === "key"
|
||||||
|
? this._getKeyConnectionCommand(this.credential.value)
|
||||||
|
: this._getTokenConnectionCommand(this.credential.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTerminalSuppressedData(): string[] {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,23 @@
|
|||||||
import { TerminalKind } from "../../../../Contracts/ViewModels";
|
import { TerminalKind } from "../../../../Contracts/ViewModels";
|
||||||
import { userContext } from "../../../../UserContext";
|
import { userContext } from "../../../../UserContext";
|
||||||
import { listKeys } from "../../../../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
import { getReadOnlyKeys, listKeys } from "../../../../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||||
|
import { acquireMsalTokenForAccount, getMsalInstance } from "../../../../Utils/AuthorizationUtils";
|
||||||
import { CassandraShellHandler } from "./CassandraShellHandler";
|
import { CassandraShellHandler } from "./CassandraShellHandler";
|
||||||
|
import { CosmosDBShellHandler } from "./CosmosDBShellHandler";
|
||||||
import { MongoShellHandler } from "./MongoShellHandler";
|
import { MongoShellHandler } from "./MongoShellHandler";
|
||||||
import { PostgresShellHandler } from "./PostgresShellHandler";
|
import { PostgresShellHandler } from "./PostgresShellHandler";
|
||||||
import { getHandler, getKey } from "./ShellTypeFactory";
|
import type { CosmosDBShellCredentialDiagnostics } from "./ShellTypeFactory";
|
||||||
|
import { getCosmosDBShellCredential, getHandler, getKey } from "./ShellTypeFactory";
|
||||||
import { VCoreMongoShellHandler } from "./VCoreMongoShellHandler";
|
import { VCoreMongoShellHandler } from "./VCoreMongoShellHandler";
|
||||||
|
|
||||||
interface UserContextType {
|
interface UserContextType {
|
||||||
databaseAccount: { name: string };
|
databaseAccount: { name: string; properties?: { disableLocalAuth?: boolean } };
|
||||||
subscriptionId: string;
|
subscriptionId: string;
|
||||||
resourceGroup: string;
|
resourceGroup: string;
|
||||||
features: { enableAadDataPlane: boolean };
|
features: { enableAadDataPlane: boolean };
|
||||||
dataPlaneRbacEnabled: boolean;
|
dataPlaneRbacEnabled: boolean;
|
||||||
aadToken?: string;
|
aadToken?: string;
|
||||||
|
masterKey?: string;
|
||||||
apiType?: string;
|
apiType?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,13 +34,29 @@ jest.mock("../../../../UserContext", () => ({
|
|||||||
|
|
||||||
jest.mock("../../../../Utils/arm/generatedClients/cosmos/databaseAccounts", () => ({
|
jest.mock("../../../../Utils/arm/generatedClients/cosmos/databaseAccounts", () => ({
|
||||||
listKeys: jest.fn(),
|
listKeys: jest.fn(),
|
||||||
|
getReadOnlyKeys: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock("../../../../Utils/AuthorizationUtils", () => {
|
||||||
|
const actual = jest.requireActual("../../../../Utils/AuthorizationUtils");
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
getMsalInstance: jest.fn(),
|
||||||
|
acquireMsalTokenForAccount: jest.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe("ShellTypeHandlerFactory", () => {
|
describe("ShellTypeHandlerFactory", () => {
|
||||||
const mockKey = "testKey";
|
const mockKey = "testKey";
|
||||||
|
|
||||||
|
const mockMsalAccounts = (accounts: { username: string }[]): void => {
|
||||||
|
(getMsalInstance as jest.Mock).mockResolvedValue({ getAllAccounts: (): { username: string }[] => accounts });
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
(listKeys as jest.Mock).mockResolvedValue({ primaryMasterKey: mockKey });
|
(listKeys as jest.Mock).mockResolvedValue({ primaryMasterKey: mockKey });
|
||||||
|
mockMsalAccounts([]);
|
||||||
|
(acquireMsalTokenForAccount as jest.Mock).mockResolvedValue("");
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -69,7 +89,7 @@ describe("ShellTypeHandlerFactory", () => {
|
|||||||
type DatabaseAccountType = { name: string };
|
type DatabaseAccountType = { name: string };
|
||||||
(userContext.databaseAccount as DatabaseAccountType).name = "";
|
(userContext.databaseAccount as DatabaseAccountType).name = "";
|
||||||
|
|
||||||
const key = await getKey();
|
const key = await getKey(false);
|
||||||
expect(key).toBe("");
|
expect(key).toBe("");
|
||||||
expect(listKeys).not.toHaveBeenCalled();
|
expect(listKeys).not.toHaveBeenCalled();
|
||||||
|
|
||||||
@@ -80,7 +100,7 @@ describe("ShellTypeHandlerFactory", () => {
|
|||||||
it("should return empty string when listKeys returns null", async () => {
|
it("should return empty string when listKeys returns null", async () => {
|
||||||
(listKeys as jest.Mock).mockResolvedValue(null);
|
(listKeys as jest.Mock).mockResolvedValue(null);
|
||||||
|
|
||||||
const key = await getKey();
|
const key = await getKey(false);
|
||||||
expect(key).toBe("");
|
expect(key).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -89,7 +109,7 @@ describe("ShellTypeHandlerFactory", () => {
|
|||||||
/* no primaryMasterKey */
|
/* no primaryMasterKey */
|
||||||
});
|
});
|
||||||
|
|
||||||
const key = await getKey();
|
const key = await getKey(false);
|
||||||
expect(key).toBe("");
|
expect(key).toBe("");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -116,12 +136,57 @@ describe("ShellTypeHandlerFactory", () => {
|
|||||||
expect(handler).toBeInstanceOf(CassandraShellHandler);
|
expect(handler).toBeInstanceOf(CassandraShellHandler);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should return CosmosDBShellHandler with key for CosmosDB terminal kind", async () => {
|
||||||
|
const handler = await getHandler(TerminalKind.CosmosDB);
|
||||||
|
expect(handler).toBeInstanceOf(CosmosDBShellHandler);
|
||||||
|
});
|
||||||
|
|
||||||
it("should get key successfully when database name exists", async () => {
|
it("should get key successfully when database name exists", async () => {
|
||||||
const key = await getKey();
|
const key = await getKey(false);
|
||||||
expect(key).toBe(mockKey);
|
expect(key).toBe(mockKey);
|
||||||
expect(listKeys).toHaveBeenCalledWith("testSubId", "testResourceGroup", "testDbName");
|
expect(listKeys).toHaveBeenCalledWith("testSubId", "testResourceGroup", "testDbName");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should return the aadToken without listing keys when Entra ID auth is requested", async () => {
|
||||||
|
(userContext as UserContextType).aadToken = "aadToken123";
|
||||||
|
|
||||||
|
const key = await getKey(true);
|
||||||
|
expect(key).toBe("aadToken123");
|
||||||
|
expect(listKeys).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return an empty string when Entra ID auth is requested but no cached token or account exists", async () => {
|
||||||
|
(userContext as UserContextType).aadToken = undefined;
|
||||||
|
mockMsalAccounts([]);
|
||||||
|
|
||||||
|
const key = await getKey(true);
|
||||||
|
expect(key).toBe("");
|
||||||
|
expect(acquireMsalTokenForAccount).not.toHaveBeenCalled();
|
||||||
|
expect(listKeys).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should silently mint a Cosmos token when Entra ID auth is requested and an MSAL account is cached", async () => {
|
||||||
|
(userContext as UserContextType).aadToken = undefined;
|
||||||
|
mockMsalAccounts([{ username: "user@contoso.com" }]);
|
||||||
|
(acquireMsalTokenForAccount as jest.Mock).mockResolvedValue("mintedToken123");
|
||||||
|
|
||||||
|
const key = await getKey(true);
|
||||||
|
expect(key).toBe("mintedToken123");
|
||||||
|
expect(acquireMsalTokenForAccount).toHaveBeenCalled();
|
||||||
|
expect(listKeys).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return an empty string when the silent token acquisition fails", async () => {
|
||||||
|
(userContext as UserContextType).aadToken = undefined;
|
||||||
|
mockMsalAccounts([{ username: "user@contoso.com" }]);
|
||||||
|
(acquireMsalTokenForAccount as jest.Mock).mockRejectedValue(new Error("interaction_required"));
|
||||||
|
jest.spyOn(console, "error").mockImplementation(() => undefined);
|
||||||
|
|
||||||
|
const key = await getKey(true);
|
||||||
|
expect(key).toBe("");
|
||||||
|
expect(listKeys).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("should return MongoShellHandler with primaryMasterKey for TerminalKind.Mongo when RBAC is disabled", async () => {
|
it("should return MongoShellHandler with primaryMasterKey for TerminalKind.Mongo when RBAC is disabled", async () => {
|
||||||
(listKeys as jest.Mock).mockResolvedValue({ primaryMasterKey: "primaryKey123" });
|
(listKeys as jest.Mock).mockResolvedValue({ primaryMasterKey: "primaryKey123" });
|
||||||
(userContext as UserContextType).features.enableAadDataPlane = false;
|
(userContext as UserContextType).features.enableAadDataPlane = false;
|
||||||
@@ -150,4 +215,148 @@ describe("ShellTypeHandlerFactory", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getCosmosDBShellCredential", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(userContext as UserContextType).aadToken = undefined;
|
||||||
|
(userContext as UserContextType).masterKey = undefined;
|
||||||
|
(userContext as UserContextType).apiType = "SQL";
|
||||||
|
(userContext as UserContextType).features.enableAadDataPlane = false;
|
||||||
|
(userContext as UserContextType).dataPlaneRbacEnabled = false;
|
||||||
|
(userContext as UserContextType).databaseAccount.properties = { disableLocalAuth: false };
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reuse the master key Data Explorer already resolved without calling ARM", async () => {
|
||||||
|
(userContext as UserContextType).masterKey = "cachedMasterKey";
|
||||||
|
|
||||||
|
const credential = await getCosmosDBShellCredential();
|
||||||
|
|
||||||
|
expect(credential).toEqual({ kind: "key", value: "cachedMasterKey" });
|
||||||
|
expect(listKeys).not.toHaveBeenCalled();
|
||||||
|
expect(getReadOnlyKeys).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fall back to the read-only keys when the caller lacks list-keys permission", async () => {
|
||||||
|
const authorizationFailed = Object.assign(new Error("AuthorizationFailed"), { code: "AuthorizationFailed" });
|
||||||
|
(listKeys as jest.Mock).mockRejectedValue(authorizationFailed);
|
||||||
|
(getReadOnlyKeys as jest.Mock).mockResolvedValue({ primaryReadonlyMasterKey: "readOnlyKey" });
|
||||||
|
|
||||||
|
const credential = await getCosmosDBShellCredential();
|
||||||
|
|
||||||
|
expect(credential).toEqual({ kind: "key", value: "readOnlyKey" });
|
||||||
|
expect(getReadOnlyKeys).toHaveBeenCalledWith("testSubId", "testResourceGroup", "testDbName");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined when both the read-write and read-only key fetches fail", async () => {
|
||||||
|
const authorizationFailed = Object.assign(new Error("AuthorizationFailed"), { code: "AuthorizationFailed" });
|
||||||
|
(listKeys as jest.Mock).mockRejectedValue(authorizationFailed);
|
||||||
|
(getReadOnlyKeys as jest.Mock).mockRejectedValue(new Error("Forbidden"));
|
||||||
|
jest.spyOn(console, "error").mockImplementation(() => undefined);
|
||||||
|
|
||||||
|
const credential = await getCosmosDBShellCredential();
|
||||||
|
|
||||||
|
expect(credential).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the account key when Entra ID auth is not enabled", async () => {
|
||||||
|
const credential = await getCosmosDBShellCredential();
|
||||||
|
|
||||||
|
expect(credential).toEqual({ kind: "key", value: mockKey });
|
||||||
|
expect(listKeys).toHaveBeenCalledWith("testSubId", "testResourceGroup", "testDbName");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the cached aadToken when Entra ID auth is enabled", async () => {
|
||||||
|
(userContext as UserContextType).dataPlaneRbacEnabled = true;
|
||||||
|
(userContext as UserContextType).aadToken = "aadToken123";
|
||||||
|
|
||||||
|
const credential = await getCosmosDBShellCredential();
|
||||||
|
|
||||||
|
expect(credential).toEqual({ kind: "token", value: "aadToken123" });
|
||||||
|
expect(listKeys).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a silently minted token when an MSAL account is cached", async () => {
|
||||||
|
(userContext as UserContextType).dataPlaneRbacEnabled = true;
|
||||||
|
mockMsalAccounts([{ username: "user@contoso.com" }]);
|
||||||
|
(acquireMsalTokenForAccount as jest.Mock).mockResolvedValue("mintedToken123");
|
||||||
|
|
||||||
|
const credential = await getCosmosDBShellCredential();
|
||||||
|
|
||||||
|
expect(credential).toEqual({ kind: "token", value: "mintedToken123" });
|
||||||
|
expect(listKeys).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fall back to the account key when no token could be resolved", async () => {
|
||||||
|
(userContext as UserContextType).dataPlaneRbacEnabled = true;
|
||||||
|
|
||||||
|
const credential = await getCosmosDBShellCredential();
|
||||||
|
|
||||||
|
expect(acquireMsalTokenForAccount).not.toHaveBeenCalled();
|
||||||
|
expect(credential).toEqual({ kind: "key", value: mockKey });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fall back to the account key when the silent token acquisition throws", async () => {
|
||||||
|
(userContext as UserContextType).dataPlaneRbacEnabled = true;
|
||||||
|
mockMsalAccounts([{ username: "user@contoso.com" }]);
|
||||||
|
(acquireMsalTokenForAccount as jest.Mock).mockRejectedValue(new Error("interaction_required"));
|
||||||
|
jest.spyOn(console, "error").mockImplementation(() => undefined);
|
||||||
|
|
||||||
|
const credential = await getCosmosDBShellCredential();
|
||||||
|
|
||||||
|
expect(credential).toEqual({ kind: "key", value: mockKey });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not fall back to the account key when local auth is disabled", async () => {
|
||||||
|
(userContext as UserContextType).databaseAccount.properties = { disableLocalAuth: true };
|
||||||
|
jest.spyOn(console, "warn").mockImplementation(() => undefined);
|
||||||
|
|
||||||
|
const credential = await getCosmosDBShellCredential();
|
||||||
|
|
||||||
|
expect(credential).toBeUndefined();
|
||||||
|
expect(listKeys).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined when listing the account keys fails", async () => {
|
||||||
|
(listKeys as jest.Mock).mockRejectedValue(new Error("Forbidden"));
|
||||||
|
jest.spyOn(console, "error").mockImplementation(() => undefined);
|
||||||
|
|
||||||
|
const credential = await getCosmosDBShellCredential();
|
||||||
|
|
||||||
|
expect(credential).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined when the database name is missing", async () => {
|
||||||
|
(userContext as UserContextType).databaseAccount.name = "";
|
||||||
|
|
||||||
|
const credential = await getCosmosDBShellCredential();
|
||||||
|
|
||||||
|
expect(credential).toBeUndefined();
|
||||||
|
expect(listKeys).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
(userContext as UserContextType).databaseAccount.name = "testDbName";
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should populate a specific reason when both key fetches fail, for the shell to echo", async () => {
|
||||||
|
(listKeys as jest.Mock).mockRejectedValue(new Error("Forbidden"));
|
||||||
|
(getReadOnlyKeys as jest.Mock).mockRejectedValue(new Error("Forbidden"));
|
||||||
|
jest.spyOn(console, "error").mockImplementation(() => undefined);
|
||||||
|
|
||||||
|
const diagnostics: CosmosDBShellCredentialDiagnostics = {};
|
||||||
|
const credential = await getCosmosDBShellCredential(diagnostics);
|
||||||
|
|
||||||
|
expect(credential).toBeUndefined();
|
||||||
|
expect(diagnostics.reason).toContain("Forbidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should populate a reason when local auth is disabled and no token was resolved", async () => {
|
||||||
|
(userContext as UserContextType).databaseAccount.properties = { disableLocalAuth: true };
|
||||||
|
jest.spyOn(console, "warn").mockImplementation(() => undefined);
|
||||||
|
|
||||||
|
const diagnostics: CosmosDBShellCredentialDiagnostics = {};
|
||||||
|
const credential = await getCosmosDBShellCredential(diagnostics);
|
||||||
|
|
||||||
|
expect(credential).toBeUndefined();
|
||||||
|
expect(diagnostics.reason).toContain("local (key) auth is disabled");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { TerminalKind } from "../../../../Contracts/ViewModels";
|
import { TerminalKind } from "../../../../Contracts/ViewModels";
|
||||||
import { userContext } from "../../../../UserContext";
|
import { userContext } from "../../../../UserContext";
|
||||||
import { listKeys } from "../../../../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
import { getReadOnlyKeys, listKeys } from "../../../../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||||
import { isDataplaneRbacEnabledForProxyApi } from "../../../../Utils/AuthorizationUtils";
|
import {
|
||||||
|
acquireMsalTokenForAccount,
|
||||||
|
getMsalInstance,
|
||||||
|
isCloudShellEntraAuthEnabled,
|
||||||
|
isDataplaneRbacEnabledForProxyApi,
|
||||||
|
} from "../../../../Utils/AuthorizationUtils";
|
||||||
import { AbstractShellHandler } from "./AbstractShellHandler";
|
import { AbstractShellHandler } from "./AbstractShellHandler";
|
||||||
import { CassandraShellHandler } from "./CassandraShellHandler";
|
import { CassandraShellHandler } from "./CassandraShellHandler";
|
||||||
|
import { CosmosDBShellCredential, CosmosDBShellHandler } from "./CosmosDBShellHandler";
|
||||||
import { MongoShellHandler } from "./MongoShellHandler";
|
import { MongoShellHandler } from "./MongoShellHandler";
|
||||||
import { PostgresShellHandler } from "./PostgresShellHandler";
|
import { PostgresShellHandler } from "./PostgresShellHandler";
|
||||||
import { VCoreMongoShellHandler } from "./VCoreMongoShellHandler";
|
import { VCoreMongoShellHandler } from "./VCoreMongoShellHandler";
|
||||||
@@ -16,23 +22,244 @@ export async function getHandler(shellType: TerminalKind): Promise<AbstractShell
|
|||||||
case TerminalKind.Postgres:
|
case TerminalKind.Postgres:
|
||||||
return new PostgresShellHandler();
|
return new PostgresShellHandler();
|
||||||
case TerminalKind.Mongo:
|
case TerminalKind.Mongo:
|
||||||
return new MongoShellHandler(await getKey());
|
return new MongoShellHandler(await getKey(isDataplaneRbacEnabledForProxyApi(userContext)));
|
||||||
case TerminalKind.VCoreMongo:
|
case TerminalKind.VCoreMongo:
|
||||||
return new VCoreMongoShellHandler();
|
return new VCoreMongoShellHandler();
|
||||||
case TerminalKind.Cassandra:
|
case TerminalKind.Cassandra:
|
||||||
return new CassandraShellHandler(await getKey());
|
return new CassandraShellHandler(await getKey(isDataplaneRbacEnabledForProxyApi(userContext)));
|
||||||
|
case TerminalKind.CosmosDB: {
|
||||||
|
const diagnostics: CosmosDBShellCredentialDiagnostics = {};
|
||||||
|
const credential = await getCosmosDBShellCredential(diagnostics);
|
||||||
|
return new CosmosDBShellHandler(credential, diagnostics.reason);
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported shell type: ${shellType}`);
|
throw new Error(`Unsupported shell type: ${shellType}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getKey(): Promise<string> {
|
/**
|
||||||
|
* Populated by {@link getCosmosDBShellCredential} (and its helpers) with a human-readable
|
||||||
|
* explanation when no credential could be resolved, so the shell can echo a specific cause
|
||||||
|
* instead of a generic message. There is no way to inspect the browser dev tools console
|
||||||
|
* from inside the remote Cloud Shell terminal, so this is surfaced directly in the shell.
|
||||||
|
*/
|
||||||
|
export interface CosmosDBShellCredentialDiagnostics {
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the credential Data Explorer already holds for the current account and hands
|
||||||
|
* it to the Cosmos DB Shell.
|
||||||
|
*
|
||||||
|
* The Cloud Shell cannot authenticate to Cosmos DB on its own: its managed identity is
|
||||||
|
* rejected with "AudienceNotSupported" for the `*.documents.azure.com` audience, its `az`
|
||||||
|
* session is not signed in, and the interactive browser/device-code flows are not usable
|
||||||
|
* from the embedded terminal. The credential therefore has to come from Data Explorer.
|
||||||
|
*
|
||||||
|
* Resolution order:
|
||||||
|
* 1. Entra ID data-plane token — the cached `userContext.aadToken`, or a silently minted
|
||||||
|
* one. Silent acquisition is only attempted when an MSAL account is already cached so
|
||||||
|
* it can never trigger an interactive popup.
|
||||||
|
* 2. Account master key — the key Data Explorer already resolved (`userContext.masterKey`),
|
||||||
|
* or, if that is not populated, one fetched via ARM (`listKeys`, falling back to
|
||||||
|
* `getReadOnlyKeys` on any failure for read-only callers). The key is handed to the
|
||||||
|
* handler, which exports it as COSMOSDB_SHELL_ACCOUNT_KEY on the same command line as
|
||||||
|
* the `cosmosdbshell` invocation. Skipped when the account has local auth disabled
|
||||||
|
* (keys do not exist).
|
||||||
|
*
|
||||||
|
* Returns `undefined` when nothing could be resolved, which makes the handler surface
|
||||||
|
* actionable guidance instead of letting the tool attempt its own sign-in.
|
||||||
|
*/
|
||||||
|
export async function getCosmosDBShellCredential(
|
||||||
|
diagnostics?: CosmosDBShellCredentialDiagnostics,
|
||||||
|
): Promise<CosmosDBShellCredential | undefined> {
|
||||||
|
const dbName = userContext.databaseAccount?.name;
|
||||||
|
if (!dbName) {
|
||||||
|
if (diagnostics) {
|
||||||
|
diagnostics.reason = "no database account name is available";
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCloudShellEntraAuthEnabled(userContext)) {
|
||||||
|
const token = await resolveCosmosDataPlaneToken();
|
||||||
|
if (token) {
|
||||||
|
return { kind: "token", value: token };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userContext.databaseAccount?.properties?.disableLocalAuth) {
|
||||||
|
const reason =
|
||||||
|
"local (key) auth is disabled on this account and no Entra ID token could be resolved; sign in via " +
|
||||||
|
'"Login for Entra ID" in the toolbar and reopen the shell';
|
||||||
|
console.warn(`CloudShell: ${reason}.`);
|
||||||
|
if (diagnostics) {
|
||||||
|
diagnostics.reason = reason;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = await resolveAccountKey(dbName, diagnostics);
|
||||||
|
if (!key) {
|
||||||
|
if (!diagnostics?.reason) {
|
||||||
|
const reason =
|
||||||
|
"no account key could be resolved. Data Explorer may be authenticating through the portal proxy " +
|
||||||
|
"(per-request tokens), which cannot be reused by the shell. Ensure you have either data-plane RBAC " +
|
||||||
|
'access (then use "Login for Entra ID") or permission to list the account keys';
|
||||||
|
console.warn(`CloudShell: ${reason}.`);
|
||||||
|
if (diagnostics) {
|
||||||
|
diagnostics.reason = reason;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return { kind: "key", value: key };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a Cosmos data-plane token without ever prompting. Silent acquisition is guarded
|
||||||
|
* on an existing cached MSAL account because `acquireMsalTokenForAccount` falls back to an
|
||||||
|
* interactive popup when none exists, which cannot complete in the hosted Cloud Shell.
|
||||||
|
*/
|
||||||
|
async function resolveCosmosDataPlaneToken(): Promise<string> {
|
||||||
|
if (userContext.aadToken) {
|
||||||
|
return userContext.aadToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const msalInstance = await getMsalInstance();
|
||||||
|
if (msalInstance.getAllAccounts().length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return (await acquireMsalTokenForAccount(userContext.databaseAccount, true)) || "";
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to silently acquire a Cosmos data-plane token for the Cloud Shell", error);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the subscription id and resource group for the current account. These are
|
||||||
|
* normally populated on `userContext`, but in some hosting contexts they can be missing,
|
||||||
|
* so they are parsed from the account's ARM resource id as a fallback. The id has the form
|
||||||
|
* `/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.DocumentDB/databaseAccounts/<name>`.
|
||||||
|
*/
|
||||||
|
function resolveAccountArmScope(): { subscriptionId: string; resourceGroup: string } {
|
||||||
|
let subscriptionId = userContext.subscriptionId;
|
||||||
|
let resourceGroup = userContext.resourceGroup;
|
||||||
|
|
||||||
|
const accountId = userContext.databaseAccount?.id;
|
||||||
|
if ((!subscriptionId || !resourceGroup) && accountId) {
|
||||||
|
const match = accountId.match(/\/subscriptions\/([^/]+)\/resourceGroups\/([^/]+)\//i);
|
||||||
|
if (match) {
|
||||||
|
subscriptionId = subscriptionId || match[1];
|
||||||
|
resourceGroup = resourceGroup || match[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { subscriptionId, resourceGroup };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveAccountKey(dbName: string, diagnostics?: CosmosDBShellCredentialDiagnostics): Promise<string> {
|
||||||
|
// Prefer the key Data Explorer already resolved for this account so the shell reuses the
|
||||||
|
// exact credential DE is connected with and avoids a redundant ARM round-trip.
|
||||||
|
if (userContext.masterKey) {
|
||||||
|
return userContext.masterKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise fetch it via ARM, mirroring DE's own `fetchAndUpdateKeys`: try the read-write
|
||||||
|
// keys first, then fall back to the read-only keys. The fallback is attempted on any
|
||||||
|
// failure (not just "AuthorizationFailed") because a read-only caller can surface the
|
||||||
|
// missing-permission error in different shapes; without it such a user gets no credential
|
||||||
|
// and the shell cannot connect.
|
||||||
|
const { subscriptionId, resourceGroup } = resolveAccountArmScope();
|
||||||
|
if (!subscriptionId || !resourceGroup) {
|
||||||
|
const reason = "the subscription id or resource group for this account could not be determined";
|
||||||
|
console.error(`CloudShell: cannot list Cosmos DB account keys because ${reason}.`);
|
||||||
|
if (diagnostics) {
|
||||||
|
diagnostics.reason = reason;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
let listKeysErrorMessage: string | undefined;
|
||||||
|
try {
|
||||||
|
const keys = await listKeys(subscriptionId, resourceGroup, dbName);
|
||||||
|
if (keys?.primaryMasterKey) {
|
||||||
|
return keys.primaryMasterKey;
|
||||||
|
}
|
||||||
|
listKeysErrorMessage = "the response did not include a usable key";
|
||||||
|
} catch (error) {
|
||||||
|
listKeysErrorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error("Failed to list read-write account keys for the Cloud Shell; trying read-only keys", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const readOnlyKeys = await getReadOnlyKeys(subscriptionId, resourceGroup, dbName);
|
||||||
|
if (readOnlyKeys?.primaryReadonlyMasterKey) {
|
||||||
|
return readOnlyKeys.primaryReadonlyMasterKey;
|
||||||
|
}
|
||||||
|
if (diagnostics) {
|
||||||
|
diagnostics.reason =
|
||||||
|
`listing account keys failed (${listKeysErrorMessage}) and the read-only keys response did not ` +
|
||||||
|
"include a usable key either";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
} catch (readOnlyError) {
|
||||||
|
console.error("Failed to list read-only account keys for the Cloud Shell", readOnlyError);
|
||||||
|
if (diagnostics) {
|
||||||
|
const readOnlyErrorMessage = readOnlyError instanceof Error ? readOnlyError.message : String(readOnlyError);
|
||||||
|
diagnostics.reason =
|
||||||
|
`listing the account keys failed (${listKeysErrorMessage}) and listing the read-only keys also failed ` +
|
||||||
|
`(${readOnlyErrorMessage}) — this is usually a missing listKeys/listReadOnlyKeys RBAC permission`;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the credential to inject into the Cloud Shell for a Mongo or Cassandra
|
||||||
|
* connection.
|
||||||
|
*
|
||||||
|
* @param useEntraIdAuth When true, returns an Entra ID (AAD) bearer token scoped to
|
||||||
|
* the account's data plane; otherwise returns the account master key. The caller
|
||||||
|
* decides which credential applies so it stays in sync with the connection command
|
||||||
|
* the matching handler builds.
|
||||||
|
*
|
||||||
|
* The Cosmos DB (NoSQL) shell does not use this function — see
|
||||||
|
* {@link getCosmosDBShellCredential}, which also reports which kind of credential it
|
||||||
|
* resolved so the correct environment variable is exported.
|
||||||
|
*/
|
||||||
|
export async function getKey(useEntraIdAuth: boolean): Promise<string> {
|
||||||
const dbName = userContext.databaseAccount.name;
|
const dbName = userContext.databaseAccount.name;
|
||||||
if (!dbName) {
|
if (!dbName) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
if (isDataplaneRbacEnabledForProxyApi(userContext)) {
|
if (useEntraIdAuth) {
|
||||||
return userContext.aadToken || "";
|
if (userContext.aadToken) {
|
||||||
|
return userContext.aadToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const msalInstance = await getMsalInstance();
|
||||||
|
if (msalInstance.getAllAccounts().length === 0) {
|
||||||
|
// No cached account to silently acquire against; a token request here would
|
||||||
|
// require an interactive popup, which cannot complete in the Cloud Shell.
|
||||||
|
console.warn(
|
||||||
|
"CloudShell: no cached MSAL account available to mint a Cosmos data-plane token; " +
|
||||||
|
"the shell will use interactive device-code authentication.",
|
||||||
|
);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const token = (await acquireMsalTokenForAccount(userContext.databaseAccount, true)) || "";
|
||||||
|
if (!token) {
|
||||||
|
console.warn("CloudShell: silent Cosmos data-plane token acquisition returned an empty token.");
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to silently acquire a Cosmos data-plane token for the Cloud Shell", error);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const keys = await listKeys(userContext.subscriptionId, userContext.resourceGroup, dbName);
|
const keys = await listKeys(userContext.subscriptionId, userContext.resourceGroup, dbName);
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { Terminal } from "xterm";
|
||||||
|
import { askConfirmation } from "./CommonUtils";
|
||||||
|
|
||||||
|
describe("askConfirmation", () => {
|
||||||
|
const createTerminalMock = () => {
|
||||||
|
let keyHandler: ((event: { key: string }) => void) | undefined;
|
||||||
|
const dispose = jest.fn();
|
||||||
|
const terminal = {
|
||||||
|
writeln: jest.fn(),
|
||||||
|
focus: jest.fn(),
|
||||||
|
onKey: jest.fn((handler: (event: { key: string }) => void) => {
|
||||||
|
keyHandler = handler;
|
||||||
|
return { dispose };
|
||||||
|
}),
|
||||||
|
} as unknown as Terminal;
|
||||||
|
|
||||||
|
return {
|
||||||
|
terminal,
|
||||||
|
dispose,
|
||||||
|
pressKey: (key: string) => keyHandler?.({ key }),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
it("resolves true when the user presses Y", async () => {
|
||||||
|
const { terminal, dispose, pressKey } = createTerminalMock();
|
||||||
|
const promise = askConfirmation(terminal, "Proceed?");
|
||||||
|
|
||||||
|
pressKey("Y");
|
||||||
|
|
||||||
|
await expect(promise).resolves.toBe(true);
|
||||||
|
expect(dispose).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves true when the user presses lowercase y", async () => {
|
||||||
|
const { terminal, pressKey } = createTerminalMock();
|
||||||
|
const promise = askConfirmation(terminal, "Proceed?");
|
||||||
|
|
||||||
|
pressKey("y");
|
||||||
|
|
||||||
|
await expect(promise).resolves.toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves false when the user presses N", async () => {
|
||||||
|
const { terminal, dispose, pressKey } = createTerminalMock();
|
||||||
|
const promise = askConfirmation(terminal, "Proceed?");
|
||||||
|
|
||||||
|
pressKey("N");
|
||||||
|
|
||||||
|
await expect(promise).resolves.toBe(false);
|
||||||
|
expect(dispose).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores keys other than Y or N and keeps listening until a valid answer", async () => {
|
||||||
|
const { terminal, dispose, pressKey } = createTerminalMock();
|
||||||
|
const promise = askConfirmation(terminal, "Proceed?");
|
||||||
|
|
||||||
|
pressKey("a");
|
||||||
|
pressKey("1");
|
||||||
|
pressKey("\r");
|
||||||
|
expect(dispose).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
pressKey("y");
|
||||||
|
|
||||||
|
await expect(promise).resolves.toBe(true);
|
||||||
|
expect(dispose).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -24,9 +24,15 @@ export const askConfirmation = async (terminal: Terminal, question: string): Pro
|
|||||||
terminal.focus();
|
terminal.focus();
|
||||||
return new Promise<boolean>((resolve) => {
|
return new Promise<boolean>((resolve) => {
|
||||||
const keyListener = terminal.onKey(({ key }: { key: string }) => {
|
const keyListener = terminal.onKey(({ key }: { key: string }) => {
|
||||||
|
const normalizedKey = key.toLowerCase();
|
||||||
|
// Only "y" or "n" are accepted. Any other key is ignored so an accidental
|
||||||
|
// keypress does not abort the flow; keep listening until a valid answer.
|
||||||
|
if (normalizedKey !== "y" && normalizedKey !== "n") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
keyListener.dispose();
|
keyListener.dispose();
|
||||||
terminal.writeln(key);
|
terminal.writeln(key);
|
||||||
return resolve(key.toLowerCase() === "y");
|
return resolve(normalizedKey === "y");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -46,6 +52,8 @@ export const getShellNameForDisplay = (terminalKind: TerminalKind): string => {
|
|||||||
case TerminalKind.Mongo:
|
case TerminalKind.Mongo:
|
||||||
case TerminalKind.VCoreMongo:
|
case TerminalKind.VCoreMongo:
|
||||||
return "MongoDB";
|
return "MongoDB";
|
||||||
|
case TerminalKind.CosmosDB:
|
||||||
|
return "Cosmos DB";
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -990,6 +990,8 @@
|
|||||||
"quantizationByteSizeTooltipContainerName": "container",
|
"quantizationByteSizeTooltipContainerName": "container",
|
||||||
"quantizationByteSizeTooltipGlobalSecondaryIndexName": "global secondary index",
|
"quantizationByteSizeTooltipGlobalSecondaryIndexName": "global secondary index",
|
||||||
"quantizerType": "Quantizer type",
|
"quantizerType": "Quantizer type",
|
||||||
|
"quantizerTypeProduct": "Product",
|
||||||
|
"quantizerTypeSpherical": "Spherical",
|
||||||
"quantizerTypeTooltip": "The quantization method used by the vector index.",
|
"quantizerTypeTooltip": "The quantization method used by the vector index.",
|
||||||
"indexingSearchListSize": "Indexing search list size",
|
"indexingSearchListSize": "Indexing search list size",
|
||||||
"vectorIndexShardKey": "Vector index shard key",
|
"vectorIndexShardKey": "Vector index shard key",
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export type Features = {
|
|||||||
readonly disableConnectionStringLogin: boolean;
|
readonly disableConnectionStringLogin: boolean;
|
||||||
readonly enableContainerCopy: boolean;
|
readonly enableContainerCopy: boolean;
|
||||||
readonly enableCloudShell: boolean;
|
readonly enableCloudShell: boolean;
|
||||||
|
readonly enableCosmosDBShell: boolean;
|
||||||
readonly enableRestoreContainer: boolean; // only for Fabric
|
readonly enableRestoreContainer: boolean; // only for Fabric
|
||||||
readonly mongoDisableNativeAuth: boolean;
|
readonly mongoDisableNativeAuth: boolean;
|
||||||
|
|
||||||
@@ -104,6 +105,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
|||||||
enableContainerCopy: "true" === get("enablecontainercopy"),
|
enableContainerCopy: "true" === get("enablecontainercopy"),
|
||||||
enableRestoreContainer: "true" === get("enablerestorecontainer"),
|
enableRestoreContainer: "true" === get("enablerestorecontainer"),
|
||||||
enableCloudShell: true,
|
enableCloudShell: true,
|
||||||
|
enableCosmosDBShell: "true" === get("enablecosmosdbshell"),
|
||||||
mongoDisableNativeAuth: "true" === get("mongodisablenativeauth"),
|
mongoDisableNativeAuth: "true" === get("mongodisablenativeauth"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ describe("AuthorizationUtils", () => {
|
|||||||
updateUserContext({
|
updateUserContext({
|
||||||
features: {
|
features: {
|
||||||
enableContainerCopy: false,
|
enableContainerCopy: false,
|
||||||
|
enableCosmosDBShell: false,
|
||||||
enableAadDataPlane: enabled,
|
enableAadDataPlane: enabled,
|
||||||
canExceedMaximumValue: false,
|
canExceedMaximumValue: false,
|
||||||
cosmosdb: false,
|
cosmosdb: false,
|
||||||
|
|||||||
@@ -236,3 +236,21 @@ export function useDataplaneRbacAuthorization(userContext: UserContext): boolean
|
|||||||
export function isDataplaneRbacEnabledForProxyApi(userContext: UserContext): boolean {
|
export function isDataplaneRbacEnabledForProxyApi(userContext: UserContext): boolean {
|
||||||
return useDataplaneRbacAuthorization(userContext) && hasProxyServer(userContext.apiType);
|
return useDataplaneRbacAuthorization(userContext) && hasProxyServer(userContext.apiType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether Cloud Shell connections should authenticate with an Entra ID
|
||||||
|
* token (COSMOSDB_SHELL_TOKEN) instead of the account master key.
|
||||||
|
*
|
||||||
|
* Returns true when data-plane RBAC authorization is in effect, or when the account
|
||||||
|
* has local (key) auth disabled — in the latter case a master key would be rejected
|
||||||
|
* by the service, so we must fall back to Entra ID even if the RBAC toggle is off.
|
||||||
|
*
|
||||||
|
* Unlike {@link isDataplaneRbacEnabledForProxyApi}, this does not require a proxy
|
||||||
|
* server, so it works for the SQL/CosmosDB API which connects directly to the
|
||||||
|
* data plane.
|
||||||
|
*/
|
||||||
|
export function isCloudShellEntraAuthEnabled(userContext: UserContext): boolean {
|
||||||
|
return (
|
||||||
|
useDataplaneRbacAuthorization(userContext) || userContext.databaseAccount?.properties?.disableLocalAuth === true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
export function getNoSqlRbacToken(): string | undefined {
|
|
||||||
let nosqlRbacToken: string | undefined;
|
|
||||||
const shardIndex = process.env.PLAYWRIGHT_SHARD_INDEX ?? "";
|
|
||||||
switch (parseInt(shardIndex)) {
|
|
||||||
case 1:
|
|
||||||
nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_1_TOKEN;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_2_TOKEN;
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_3_TOKEN;
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_4_TOKEN;
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_5_TOKEN;
|
|
||||||
break;
|
|
||||||
case 6:
|
|
||||||
nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_6_TOKEN;
|
|
||||||
break;
|
|
||||||
case 7:
|
|
||||||
nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_7_TOKEN;
|
|
||||||
break;
|
|
||||||
case 8:
|
|
||||||
nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_8_TOKEN;
|
|
||||||
break;
|
|
||||||
case 9:
|
|
||||||
nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_9_TOKEN;
|
|
||||||
break;
|
|
||||||
case 10:
|
|
||||||
nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_10_TOKEN;
|
|
||||||
break;
|
|
||||||
case 11:
|
|
||||||
nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_11_TOKEN;
|
|
||||||
break;
|
|
||||||
case 12:
|
|
||||||
nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_12_TOKEN;
|
|
||||||
break;
|
|
||||||
case 13:
|
|
||||||
nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_13_TOKEN;
|
|
||||||
break;
|
|
||||||
case 14:
|
|
||||||
nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_14_TOKEN;
|
|
||||||
break;
|
|
||||||
case 15:
|
|
||||||
nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_15_TOKEN;
|
|
||||||
break;
|
|
||||||
case 16:
|
|
||||||
nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_16_TOKEN;
|
|
||||||
break;
|
|
||||||
case 17:
|
|
||||||
nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_17_TOKEN;
|
|
||||||
break;
|
|
||||||
case 18:
|
|
||||||
nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_18_TOKEN;
|
|
||||||
break;
|
|
||||||
case 19:
|
|
||||||
nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_19_TOKEN;
|
|
||||||
break;
|
|
||||||
case 20:
|
|
||||||
nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_20_TOKEN;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!nosqlRbacToken) {
|
|
||||||
console.warn(`No NoSQL RBAC token found for shard index ${shardIndex}`);
|
|
||||||
}
|
|
||||||
return nosqlRbacToken;
|
|
||||||
}
|
|
||||||
+21
-12
@@ -1,7 +1,6 @@
|
|||||||
import { DefaultAzureCredential } from "@azure/identity";
|
import { DefaultAzureCredential } from "@azure/identity";
|
||||||
import { Frame, Locator, Page, expect } from "@playwright/test";
|
import { Frame, Locator, Page, expect } from "@playwright/test";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { getNoSqlRbacToken } from "./NoSqlTestSetup";
|
|
||||||
import { TestContainerContext } from "./testData";
|
import { TestContainerContext } from "./testData";
|
||||||
|
|
||||||
const RETRY_COUNT = 3;
|
const RETRY_COUNT = 3;
|
||||||
@@ -45,29 +44,34 @@ export enum TestAccount {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getDefaultAccountName(accountType: TestAccount): string {
|
export function getDefaultAccountName(accountType: TestAccount): string {
|
||||||
|
const accountNamePrefix = process.env.DE_ACCOUNT_PREFIX;
|
||||||
|
if (!accountNamePrefix) {
|
||||||
|
throw new Error("DE_ACCOUNT_PREFIX is not set");
|
||||||
|
}
|
||||||
|
|
||||||
switch (accountType) {
|
switch (accountType) {
|
||||||
case TestAccount.Tables:
|
case TestAccount.Tables:
|
||||||
return "github-e2etests-tables";
|
return `${accountNamePrefix}-de-test-table-1`;
|
||||||
case TestAccount.Cassandra:
|
case TestAccount.Cassandra:
|
||||||
return "github-e2etests-cassandra";
|
return `${accountNamePrefix}-de-test-cassandra-1`;
|
||||||
case TestAccount.Gremlin:
|
case TestAccount.Gremlin:
|
||||||
return "github-e2etests-gremlin";
|
return `${accountNamePrefix}-de-test-gremlin-1`;
|
||||||
case TestAccount.Mongo:
|
case TestAccount.Mongo:
|
||||||
return "github-e2etests-mongo";
|
return `${accountNamePrefix}-de-test-mongo-1`;
|
||||||
case TestAccount.MongoReadonly:
|
case TestAccount.MongoReadonly:
|
||||||
return "github-e2etests-mongo-readonly";
|
return `${accountNamePrefix}-de-test-mongo-readonly`;
|
||||||
case TestAccount.Mongo32:
|
case TestAccount.Mongo32:
|
||||||
return "github-e2etests-mongo32";
|
return `${accountNamePrefix}-de-test-mongo32-1`;
|
||||||
case TestAccount.SQLReadOnly:
|
case TestAccount.SQLReadOnly:
|
||||||
return "github-e2etests-sql-readonly";
|
return `${accountNamePrefix}-de-test-sql-readonly`;
|
||||||
case TestAccount.SQLContainerCopyOnly:
|
case TestAccount.SQLContainerCopyOnly:
|
||||||
return "github-e2etests-sql-containercopyonly";
|
return `${accountNamePrefix}-de-test-sql-containercopy`;
|
||||||
case TestAccount.SQL: {
|
case TestAccount.SQL: {
|
||||||
const shardIndex = process.env.PLAYWRIGHT_SHARD_INDEX ?? "";
|
const shardIndex = process.env.PLAYWRIGHT_SHARD_INDEX ?? "";
|
||||||
if (!shardIndex) {
|
if (!shardIndex) {
|
||||||
throw new Error("PLAYWRIGHT_SHARD_INDEX is not set");
|
throw new Error("PLAYWRIGHT_SHARD_INDEX is not set");
|
||||||
}
|
}
|
||||||
return "github-e2etests-sql-" + shardIndex;
|
return `${accountNamePrefix}-de-test-sql-${shardIndex}`;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error(`No default account name defined for account type ${accountType}`);
|
throw new Error(`No default account name defined for account type ${accountType}`);
|
||||||
@@ -75,7 +79,7 @@ export function getDefaultAccountName(accountType: TestAccount): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const resourceGroupName = process.env.DE_TEST_RESOURCE_GROUP ?? "de-e2e-tests";
|
export const resourceGroupName = process.env.DE_TEST_RESOURCE_GROUP ?? "de-e2e-tests";
|
||||||
export const subscriptionId = process.env.DE_TEST_SUBSCRIPTION_ID ?? "69e02f2d-f059-4409-9eac-97e8a276ae2c";
|
export const subscriptionId = process.env.DE_TEST_SUBSCRIPTION_ID ?? process.env.AZURE_SUBSCRIPTION_ID ?? "";
|
||||||
export const TEST_AUTOSCALE_THROUGHPUT_RU = 1000;
|
export const TEST_AUTOSCALE_THROUGHPUT_RU = 1000;
|
||||||
export const TEST_MANUAL_THROUGHPUT_RU = 800;
|
export const TEST_MANUAL_THROUGHPUT_RU = 800;
|
||||||
export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_2K = 2000;
|
export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_2K = 2000;
|
||||||
@@ -117,11 +121,16 @@ export async function getTestExplorerUrl(accountType: TestAccount, options?: Tes
|
|||||||
params.set("subscriptionId", subscriptionId);
|
params.set("subscriptionId", subscriptionId);
|
||||||
params.set("token", token);
|
params.set("token", token);
|
||||||
|
|
||||||
|
const tenantId = process.env.AZURE_TENANT_ID;
|
||||||
|
if (tenantId) {
|
||||||
|
params.set("tenantId", tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
// There seem to be occasional CORS issues with calling the copilot APIs (/api/tokens/sampledataconnection/v2, for example)
|
// There seem to be occasional CORS issues with calling the copilot APIs (/api/tokens/sampledataconnection/v2, for example)
|
||||||
// For now, since we don't test copilot, we can disable the copilot APIs by setting the feature flag to false.
|
// For now, since we don't test copilot, we can disable the copilot APIs by setting the feature flag to false.
|
||||||
params.set("feature.enableCopilot", "false");
|
params.set("feature.enableCopilot", "false");
|
||||||
|
|
||||||
const nosqlRbacToken = getNoSqlRbacToken();
|
const nosqlRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN;
|
||||||
|
|
||||||
const nosqlReadOnlyRbacToken = process.env.NOSQL_READONLY_TESTACCOUNT_TOKEN;
|
const nosqlReadOnlyRbacToken = process.env.NOSQL_READONLY_TESTACCOUNT_TOKEN;
|
||||||
const nosqlContainerCopyRbacToken = process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN;
|
const nosqlContainerCopyRbacToken = process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect, test } from "@playwright/test";
|
import { expect, test } from "@playwright/test";
|
||||||
import { setupCORSBypass } from "../CORSBypass";
|
import { setupCORSBypass } from "../CORSBypass";
|
||||||
import { DataExplorer, QueryTab, TestAccount, CommandBarButton, Editor } from "../fx";
|
import { CommandBarButton, DataExplorer, Editor, QueryTab, TestAccount } from "../fx";
|
||||||
import { serializeMongoToJson } from "../testData";
|
import { serializeMongoToJson } from "../testData";
|
||||||
|
|
||||||
const databaseId = "test-e2etests-mongo-pagination";
|
const databaseId = "test-e2etests-mongo-pagination";
|
||||||
|
|||||||
@@ -11,10 +11,9 @@ import {
|
|||||||
resourceGroupName,
|
resourceGroupName,
|
||||||
subscriptionId,
|
subscriptionId,
|
||||||
} from "../fx";
|
} from "../fx";
|
||||||
import { getNoSqlRbacToken } from "../NoSqlTestSetup";
|
|
||||||
|
|
||||||
test("SQL account using Resource token", async ({ page }) => {
|
test("SQL account using Resource token", async ({ page }) => {
|
||||||
const nosqlAccountRbacToken = getNoSqlRbacToken() ?? "";
|
const nosqlAccountRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN ?? "";
|
||||||
test.skip(nosqlAccountRbacToken.length > 0, "Resource tokens not supported when using data plane RBAC.");
|
test.skip(nosqlAccountRbacToken.length > 0, "Resource tokens not supported when using data plane RBAC.");
|
||||||
|
|
||||||
const credentials = getAzureCLICredentials();
|
const credentials = getAzureCLICredentials();
|
||||||
|
|||||||
+1
-2
@@ -18,7 +18,6 @@ import {
|
|||||||
subscriptionId,
|
subscriptionId,
|
||||||
TestAccount,
|
TestAccount,
|
||||||
} from "./fx";
|
} from "./fx";
|
||||||
import { getNoSqlRbacToken } from "./NoSqlTestSetup";
|
|
||||||
|
|
||||||
// In Node.js >= 19, globalThis.crypto is already available as a read-only getter.
|
// In Node.js >= 19, globalThis.crypto is already available as a read-only getter.
|
||||||
// Only assign the polyfill for older versions.
|
// Only assign the polyfill for older versions.
|
||||||
@@ -135,7 +134,7 @@ async function createCosmosClientForSQLAccount(
|
|||||||
|
|
||||||
const rbacToken =
|
const rbacToken =
|
||||||
accountType === TestAccount.SQL
|
accountType === TestAccount.SQL
|
||||||
? getNoSqlRbacToken()
|
? process.env.NOSQL_TESTACCOUNT_TOKEN
|
||||||
: accountType === TestAccount.SQLContainerCopyOnly
|
: accountType === TestAccount.SQLContainerCopyOnly
|
||||||
? process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN
|
? process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN
|
||||||
: "";
|
: "";
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import "../../less/hostedexplorer.less";
|
|||||||
import { DataExplorerInputsFrame } from "../../src/Contracts/ViewModels";
|
import { DataExplorerInputsFrame } from "../../src/Contracts/ViewModels";
|
||||||
import { updateUserContext } from "../../src/UserContext";
|
import { updateUserContext } from "../../src/UserContext";
|
||||||
import { get, listKeys } from "../../src/Utils/arm/generatedClients/cosmos/databaseAccounts";
|
import { get, listKeys } from "../../src/Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||||
import { getNoSqlRbacToken } from "../NoSqlTestSetup";
|
|
||||||
|
|
||||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||||
const resourceGroup = urlSearchParams.get("resourceGroup") || process.env.RESOURCE_GROUP || "";
|
const resourceGroup = urlSearchParams.get("resourceGroup") || process.env.RESOURCE_GROUP || "";
|
||||||
@@ -16,7 +15,7 @@ const enablecontainercopy = urlSearchParams.get("enablecontainercopy");
|
|||||||
|
|
||||||
const nosqlRbacToken =
|
const nosqlRbacToken =
|
||||||
urlSearchParams.get("nosqlRbacToken") ||
|
urlSearchParams.get("nosqlRbacToken") ||
|
||||||
(enablecontainercopy ? process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN : getNoSqlRbacToken()) ||
|
(enablecontainercopy ? process.env.NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN : process.env.NOSQL_TESTACCOUNT_TOKEN) ||
|
||||||
"";
|
"";
|
||||||
|
|
||||||
const nosqlReadOnlyRbacToken =
|
const nosqlReadOnlyRbacToken =
|
||||||
@@ -30,6 +29,7 @@ const mongoRbacToken = urlSearchParams.get("mongoRbacToken") || process.env.MONG
|
|||||||
const mongo32RbacToken = urlSearchParams.get("mongo32RbacToken") || process.env.MONGO32_TESTACCOUNT_TOKEN || "";
|
const mongo32RbacToken = urlSearchParams.get("mongo32RbacToken") || process.env.MONGO32_TESTACCOUNT_TOKEN || "";
|
||||||
const mongoReadOnlyRbacToken =
|
const mongoReadOnlyRbacToken =
|
||||||
urlSearchParams.get("mongoReadOnlyRbacToken") || process.env.MONGO_READONLY_TESTACCOUNT_TOKEN || "";
|
urlSearchParams.get("mongoReadOnlyRbacToken") || process.env.MONGO_READONLY_TESTACCOUNT_TOKEN || "";
|
||||||
|
const tenantId = urlSearchParams.get("tenantId") || process.env.AZURE_TENANT_ID || "";
|
||||||
|
|
||||||
const initTestExplorer = async (): Promise<void> => {
|
const initTestExplorer = async (): Promise<void> => {
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
@@ -51,7 +51,7 @@ const initTestExplorer = async (): Promise<void> => {
|
|||||||
case "gremlin":
|
case "gremlin":
|
||||||
rbacToken = gremlinRbacToken;
|
rbacToken = gremlinRbacToken;
|
||||||
break;
|
break;
|
||||||
case "tables":
|
case "table":
|
||||||
rbacToken = tableRbacToken;
|
rbacToken = tableRbacToken;
|
||||||
break;
|
break;
|
||||||
case "cassandra":
|
case "cassandra":
|
||||||
@@ -90,6 +90,7 @@ const initTestExplorer = async (): Promise<void> => {
|
|||||||
resourceGroup,
|
resourceGroup,
|
||||||
authorizationToken: `Bearer ${authToken}`,
|
authorizationToken: `Bearer ${authToken}`,
|
||||||
aadToken: rbacToken,
|
aadToken: rbacToken,
|
||||||
|
tenantId,
|
||||||
features: {},
|
features: {},
|
||||||
containerCopyEnabled: enablecontainercopy === "true",
|
containerCopyEnabled: enablecontainercopy === "true",
|
||||||
hasWriteAccess: true,
|
hasWriteAccess: true,
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ const { CosmosDBManagementClient } = require("@azure/arm-cosmosdb");
|
|||||||
const ms = require("ms");
|
const ms = require("ms");
|
||||||
|
|
||||||
const subscriptionId = process.env["AZURE_SUBSCRIPTION_ID"];
|
const subscriptionId = process.env["AZURE_SUBSCRIPTION_ID"];
|
||||||
const resourceGroupName = "de-e2e-tests";
|
const resourceGroupName = process.env["E2ETESTS_RESOURCEGROUP_NAME"];
|
||||||
|
|
||||||
const thirtyMinutesAgo = new Date(Date.now() - 1000 * 60 * 30).getTime();
|
const thirtyMinutesAgo = new Date(Date.now() - 1000 * 60 * 30).getTime();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user