Replacing all azdata with az (#16502)

* Changed azdata to az in azcli extension and resource-deployment, and some arc. Removed user, pass, url from controller connect blade. Commented out tests. Ported over work from old branch.

* Changed unit tests, all unit tests passing. Changed parameters to new ones, fixed some Controller Connect issues.

* Connect data controller and create dc working.

* Changed az back to azdata in necessary places in resource-deployment.

* Changed notebook values and added namespace to some params.

* Added some changes from PR to this branch

* Changed azdata.ts to az.ts and changed subscription parameter

* Brought over changes from azcli PR into this branch.

* added endpoint, username, password to getIsPassword

* Changed notebooks to use proper az params, hard coded in some values to verify it is working, removed some variableNames from package.json.

* Changed -sc to --storage-class in notebook

* Added namespace to SQL deploy, deleted dc create in api

* Deleted more dc create code and uncommented findAz() with unfinished work on Do Not Ask Again.

* Removed (preview) from extensions/arc and extensions/azcli excluding preview:true in package.json

* Commented out install/update prompts until DoNotAskAgain is implemented

* Fixed bugs: JSON Output errors are now being caught, --infrastructure now has a required UI component with dropdown options, config page loads properly, SQL create flags use full names instead of shortnames.

* Adds validation to pg extensions and bug fixes (#16486)

* Extensions

* Server parameters

* Change locaiton of postgres extensions, pr fixes

* Change location of list

* List spacing

* Commented out Don't Ask Again prompt implementation.

* Uncommented header of a test file.

* Added Azure CLI arcdata extension to Prerequisites

* Reverted package.json and yarn.lock

* Took away casting of stderr and stdout in executeCommand.

* Deleted override function for initializeFields in connectControllerDialog.ts

* Removed fakeAzApi for testing and added back in (Preview)

* Removed en-us from python notebook links.

* Deleted azdata tool from tool tests in resource-deployment

* Deleted another instance of azdata in tool test

* Add back in azdata tooltype

* Remove en-us

* Replaced AzdataTool in typings

* Reverting adding azdata tool back in

* Changed Azdata to AzdataToolOld

* Added back azdata tool type

* Added AzdataToolOld to tool types

* fix test

Co-authored-by: Candice Ye <canye@microsoft.com>
Co-authored-by: nasc17 <nasc@microsoft.com>
Co-authored-by: nasc17 <69922333+nasc17@users.noreply.github.com>
Co-authored-by: chgagnon <chgagnon@microsoft.com>
This commit is contained in:
Candice Ye
2021-08-01 15:12:24 -07:00
committed by GitHub
parent 65cc61fdbd
commit 914fe8fc29
58 changed files with 1623 additions and 2032 deletions

View File

@@ -2,8 +2,6 @@
Welcome to Microsoft Azure Arc Extension for Azure Data Studio!
**This extension is only applicable to customers in the Azure Arc data services public preview.**
## Overview
This extension adds the following features to Azure Data Studio.

View File

@@ -47,7 +47,8 @@
"|Tools|Description|Installation|\n",
"|---|---|---|\n",
"|kubectl | Command-line tool for monitoring the underlying Kubernetes cluster | [Installation](https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl-binary-using-native-package-management) |\n",
"|Azure Data CLI (azdata) | Command-line tool for installing and managing resources in an Azure Arc cluster |[Installation](https://docs.microsoft.com/sql/azdata/install/deploy-install-azdata) |"
"|Azure CLI (az) | Command-line tool for installing and managing resources in an Azure Arc cluster |[Installation](https://docs.microsoft.com/cli/azure/install-azure-cli-windows?tabs=azure-cli) |\n",
"|Azure CLI arcdata extension | Commands for using Azure Arc for Azure data services. | [Installation](https://docs.microsoft.com/azure/azure-arc/data/install-arcdata-extension)"
],
"metadata": {
"azdata_cell_guid": "714582b9-10ee-409e-ab12-15a4825c9471"
@@ -64,8 +65,9 @@
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"import sys,os,json,html,getpass,time, tempfile\n",
"import sys,os,getpass\n",
"def run_command(command):\n",
" print(\"Executing: \" + command)\n",
" !{command}\n",
@@ -73,12 +75,11 @@
" sys.exit(f'Command execution failed with exit code: {str(_exit_code)}.\\n\\t{command}\\n')\n",
" print(f'Successfully executed: {command}')"
],
"outputs": [],
"metadata": {
"azdata_cell_guid": "d973d5b4-7f0a-4a9d-b204-a16480f3940d",
"tags": []
},
"outputs": [],
"execution_count": null
}
},
{
"cell_type": "markdown",
@@ -101,15 +102,15 @@
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"run_command('azdata --version')"
"run_command('az --version')"
],
"outputs": [],
"metadata": {
"azdata_cell_guid": "691671d7-3f05-406c-a183-4cff7d17f83d",
"tags": []
},
"outputs": [],
"execution_count": null
}
},
{
"cell_type": "markdown",
@@ -122,6 +123,7 @@
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"if \"AZDATA_NB_VAR_ARC_ADMIN_PASSWORD\" in os.environ:\n",
" arc_admin_password = os.environ[\"AZDATA_NB_VAR_ARC_ADMIN_PASSWORD\"]\n",
@@ -134,12 +136,11 @@
" if arc_admin_password != confirm_password:\n",
" sys.exit(f'Passwords do not match.')"
],
"outputs": [],
"metadata": {
"azdata_cell_guid": "e7e10828-6cae-45af-8c2f-1484b6d4f9ac",
"tags": []
},
"outputs": [],
"execution_count": null
}
},
{
"cell_type": "markdown",
@@ -152,17 +153,17 @@
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"os.environ[\"KUBECONFIG\"] = arc_config_file\n",
"run_command(f'kubectl config use-context {arc_cluster_context}')\n",
"run_command('kubectl config current-context')"
],
"outputs": [],
"metadata": {
"azdata_cell_guid": "7d1a03d4-1df8-48eb-bff0-0042603b95b1",
"tags": []
},
"outputs": [],
"execution_count": null
}
},
{
"cell_type": "markdown",
@@ -175,22 +176,22 @@
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"print (f'Creating Azure Arc Data Controller: {arc_data_controller_name} using configuration {arc_cluster_context}')\n",
"os.environ[\"ACCEPT_EULA\"] = 'yes'\n",
"os.environ[\"AZDATA_USERNAME\"] = arc_admin_username\n",
"os.environ[\"AZDATA_PASSWORD\"] = arc_admin_password\n",
"\n",
"if os.name == 'nt':\n",
" print(f'If you don\\'t see output produced by azdata, you can run the following command in a terminal window to check the deployment status:\\n\\t {os.environ[\"AZDATA_NB_VAR_KUBECTL\"]} get pods -n {arc_data_controller_namespace}')\n",
"run_command(f'azdata arc dc create --connectivity-mode Indirect -n {arc_data_controller_name} -ns {arc_data_controller_namespace} -s {arc_subscription} -g {arc_resource_group} -l {arc_data_controller_location} -sc {arc_data_controller_storage_class} --profile-name {arc_profile}')\n",
" print(f'If you don\\'t see output produced by az, you can run the following command in a terminal window to check the deployment status:\\n\\t {os.environ[\"AZDATA_NB_VAR_KUBECTL\"]} get pods -n {arc_data_controller_namespace}')\n",
"run_command(f'az arcdata dc create --connectivity-mode indirect --name {arc_data_controller_name} --k8s-namespace {arc_data_controller_namespace} --subscription {arc_subscription} --resource-group {arc_resource_group} --location {arc_data_controller_location} --storage-class {arc_data_controller_storage_class} --profile-name {arc_profile} --infrastructure {arc_infrastructure} --use-k8s')\n",
"print(f'Azure Arc Data Controller: {arc_data_controller_name} created.') "
],
"outputs": [],
"metadata": {
"azdata_cell_guid": "373947a1-90b9-49ee-86f4-17a4c7d4ca76",
"tags": []
},
"outputs": [],
"execution_count": null
}
},
{
"cell_type": "markdown",
@@ -203,38 +204,16 @@
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"# Setting context to Data Controller.\n",
"#\n",
"run_command(f'kubectl config set-context --current --namespace {arc_data_controller_namespace}')"
],
"outputs": [],
"metadata": {
"azdata_cell_guid": "c974561f-13d0-4e7a-b74b-d781c2e06d68"
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### **Login to the Data Controller.**\n"
],
"metadata": {
"azdata_cell_guid": "9376b2ab-0edf-478f-9e3c-5ff46ae3501a"
}
},
{
"cell_type": "code",
"source": [
"# Login to the Data Controller.\n",
"#\n",
"run_command(f'azdata login --namespace {arc_data_controller_namespace}')"
],
"metadata": {
"azdata_cell_guid": "9aed0c5a-2c8a-4ad7-becb-60281923a196"
},
"outputs": [],
"execution_count": null
}
]
}
}

View File

@@ -47,7 +47,8 @@
" \n",
"|Tools|Description|Installation|\n",
"|---|---|---|\n",
"|Azure Data CLI (azdata) | Command-line tool for installing and managing resources in an Azure Arc cluster |[Installation](https://docs.microsoft.com/sql/azdata/install/deploy-install-azdata) |"
"|Azure CLI (az) | Command-line tool for installing and managing resources in an Azure Arc cluster |[Installation](https://docs.microsoft.com/cli/azure/install-azure-cli-windows?tabs=azure-cli) |\n",
"|Azure CLI arcdata extension | Commands for using Azure Arc for Azure data services. | [Installation](https://docs.microsoft.com/azure/azure-arc/data/install-arcdata-extension)"
],
"metadata": {
"azdata_cell_guid": "20fe3985-a01e-461c-bce0-235f7606cc3c"
@@ -64,6 +65,7 @@
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"import sys,os,json,subprocess\n",
"def run_command():\n",
@@ -76,16 +78,14 @@
" print(f'Successfully executed: {cmd}')\n",
" print(f'\\t>>>Output: {output.stdout.decode(\"utf-8\")}\\n')\n",
" return output.stdout.decode(\"utf-8\")\n",
"cmd = 'azdata --version'\n",
"out = run_command()\n",
""
"cmd = 'az --version'\n",
"out = run_command()\n"
],
"outputs": [],
"metadata": {
"azdata_cell_guid": "749d8dba-3da8-46e9-ae48-2b38056ab7a2",
"tags": []
},
"outputs": [],
"execution_count": null
}
},
{
"cell_type": "markdown",
@@ -111,48 +111,17 @@
},
{
"cell_type": "code",
"source": [
"# Login to the data controller.\n",
"#\n",
"os.environ[\"AZDATA_PASSWORD\"] = os.environ[\"AZDATA_NB_VAR_CONTROLLER_PASSWORD\"]\n",
"os.environ[\"KUBECONFIG\"] = controller_kubeconfig\n",
"os.environ[\"KUBECTL_CONTEXT\"] = controller_kubectl_context\n",
"endpoint_option = f' -e {controller_endpoint}' if controller_endpoint else \"\"\n",
"cmd = f'azdata login --namespace {arc_data_controller_namespace} -u {controller_username}{endpoint_option}'\n",
"out=run_command()"
],
"metadata": {
"azdata_cell_guid": "71366399-5963-4e24-b2f2-6bb5bffba4ec"
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"print (f'Creating the PostgreSQL Hyperscale - Azure Arc instance')\n",
"\n",
"workers_option = f' -w {postgres_server_group_workers}' if postgres_server_group_workers else \"\"\n",
"port_option = f' --port \"{postgres_server_group_port}\"' if postgres_server_group_port else \"\"\n",
"engine_version_option = f' -ev {postgres_server_group_engine_version}' if postgres_server_group_engine_version else \"\"\n",
"extensions_option = f' --extensions \"{postgres_server_group_extensions}\"' if postgres_server_group_extensions else \"\"\n",
"volume_size_data_option = f' -vsd {postgres_server_group_volume_size_data}Gi' if postgres_server_group_volume_size_data else \"\"\n",
"volume_size_logs_option = f' -vsl {postgres_server_group_volume_size_logs}Gi' if postgres_server_group_volume_size_logs else \"\"\n",
"volume_size_backups_option = f' -vsb {postgres_server_group_volume_size_backups}Gi' if postgres_server_group_volume_size_backups else \"\"\n",
"cores_request_option = f' -cr \"c={postgres_server_group_coordinator_cores_request},w={postgres_server_group_workers_cores_request}\"' if postgres_server_group_coordinator_cores_request and postgres_server_group_workers_cores_request else f' -cr \"c={postgres_server_group_coordinator_cores_request}\"' if postgres_server_group_coordinator_cores_request else f' -cr \"w={postgres_server_group_workers_cores_request}\"' if postgres_server_group_workers_cores_request else \"\"\n",
"cores_limit_option = f' -cl \"c={postgres_server_group_coordinator_cores_limit},w={postgres_server_group_workers_cores_limit}\"' if postgres_server_group_coordinator_cores_limit and postgres_server_group_workers_cores_limit else f' -cl \"c={postgres_server_group_coordinator_cores_limit}\"' if postgres_server_group_coordinator_cores_limit else f' -cl \"w={postgres_server_group_workers_cores_limit}\"' if postgres_server_group_workers_cores_limit else \"\"\n",
"memory_request_option = f' -mr \"c={postgres_server_group_coordinator_memory_request}Gi,w={postgres_server_group_workers_memory_request}Gi\"' if postgres_server_group_coordinator_memory_request and postgres_server_group_workers_memory_request else f' -mr \"c={postgres_server_group_coordinator_memory_request}Gi\"' if postgres_server_group_coordinator_memory_request else f' -mr \"w={postgres_server_group_workers_memory_request}Gi\"' if postgres_server_group_workers_memory_request else \"\"\n",
"memory_limit_option = f' -ml \"c={postgres_server_group_coordinator_memory_limit}Gi,w={postgres_server_group_workers_memory_limit}Gi\"' if postgres_server_group_coordinator_memory_limit and postgres_server_group_workers_memory_limit else f' -ml \"c={postgres_server_group_coordinator_memory_limit}Gi\"' if postgres_server_group_coordinator_memory_limit else f' -ml \"w={postgres_server_group_workers_memory_limit}Gi\"' if postgres_server_group_workers_memory_limit else \"\"\n",
"\n",
"os.environ[\"AZDATA_PASSWORD\"] = os.environ[\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_PASSWORD\"]\n",
"cmd = f'azdata arc postgres server create -n {postgres_server_group_name} -scd {postgres_storage_class_data} -scl {postgres_storage_class_logs} -scb {postgres_storage_class_backups}{workers_option}{port_option}{engine_version_option}{extensions_option}{volume_size_data_option}{volume_size_logs_option}{volume_size_backups_option}{cores_request_option}{cores_limit_option}{memory_request_option}{memory_limit_option}'\n",
"cmd = f'az postgres arc-server create --name {postgres_server_group_name} --k8s-namespace {arc_data_controller_namespace} --use-k8s'\n",
"out=run_command()"
],
"outputs": [],
"metadata": {
"azdata_cell_guid": "4fbaf071-55a1-40bc-be7e-7b9b5547b886"
},
"outputs": [],
"execution_count": null
}
}
]
}
}

View File

@@ -47,7 +47,8 @@
" \n",
"|Tools|Description|Installation|\n",
"|---|---|---|\n",
"|Azure Data CLI (azdata) | Command-line tool for installing and managing resources in an Azure Arc cluster |[Installation](https://docs.microsoft.com/sql/azdata/install/deploy-install-azdata) |"
"|Azure CLI (az) | Command-line tool for installing and managing resources in an Azure Arc cluster |[Installation](https://docs.microsoft.com/cli/azure/install-azure-cli-windows?tabs=azure-cli) |\n",
"|Azure CLI arcdata extension | Commands for using Azure Arc for Azure data services. | [Installation](https://docs.microsoft.com/azure/azure-arc/data/install-arcdata-extension)"
],
"metadata": {
"azdata_cell_guid": "d1c8258e-9efd-4380-a48c-cd675423ed2f"
@@ -64,6 +65,7 @@
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"import sys,os,json,subprocess\n",
"def run_command():\n",
@@ -76,16 +78,14 @@
" print(f'Successfully executed: {cmd}')\n",
" print(f'\\t>>>Output: {output.stdout.decode(\"utf-8\")}\\n')\n",
" return output.stdout.decode(\"utf-8\")\n",
"cmd = 'azdata --version'\n",
"out = run_command()\n",
""
"cmd = 'az --version'\n",
"out = run_command()\n"
],
"outputs": [],
"metadata": {
"azdata_cell_guid": "749d8dba-3da8-46e9-ae48-2b38056ab7a2",
"tags": []
},
"outputs": [],
"execution_count": null
}
},
{
"cell_type": "markdown",
@@ -111,41 +111,24 @@
},
{
"cell_type": "code",
"source": [
"# Login to the data controller.\n",
"#\n",
"os.environ[\"AZDATA_PASSWORD\"] = os.environ[\"AZDATA_NB_VAR_CONTROLLER_PASSWORD\"]\n",
"os.environ[\"KUBECONFIG\"] = controller_kubeconfig\n",
"os.environ[\"KUBECTL_CONTEXT\"] = controller_kubectl_context\n",
"cmd = f'azdata login -e {controller_endpoint} -u {controller_username}'\n",
"out=run_command()"
],
"metadata": {
"azdata_cell_guid": "1437c536-17e8-4a7f-80c1-aa43ad02686c"
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"print (f'Creating the SQL managed instance - Azure Arc instance')\n",
"\n",
"cores_request_option = f' -cr \"{sql_cores_request}\"' if sql_cores_request else \"\"\n",
"cores_limit_option = f' -cl \"{sql_cores_limit}\"' if sql_cores_limit else \"\"\n",
"memory_request_option = f' -mr \"{sql_memory_request}Gi\"' if sql_memory_request else \"\"\n",
"memory_limit_option = f' -ml \"{sql_memory_limit}Gi\"' if sql_memory_limit else \"\"\n",
"cores_request_option = f' --cores-request \"{sql_cores_request}\"' if sql_cores_request else \"\"\n",
"cores_limit_option = f' --cores-limit \"{sql_cores_limit}\"' if sql_cores_limit else \"\"\n",
"memory_request_option = f' --memory-request \"{sql_memory_request}Gi\"' if sql_memory_request else \"\"\n",
"memory_limit_option = f' --memory-limit \"{sql_memory_limit}Gi\"' if sql_memory_limit else \"\"\n",
"\n",
"os.environ[\"AZDATA_USERNAME\"] = sql_username\n",
"os.environ[\"AZDATA_PASSWORD\"] = os.environ[\"AZDATA_NB_VAR_SQL_PASSWORD\"]\n",
"cmd = f'azdata arc sql mi create -n {sql_instance_name} -scd {sql_storage_class_data} -scl {sql_storage_class_logs} --replicas {sql_replicas}{cores_request_option}{cores_limit_option}{memory_request_option}{memory_limit_option}'\n",
"cmd = f'az sql mi-arc create --name {sql_instance_name} --k8s-namespace {arc_data_controller_namespace} --replicas {sql_replicas}{cores_request_option}{cores_limit_option}{memory_request_option}{memory_limit_option} --use-k8s'\n",
"out=run_command()"
],
"outputs": [],
"metadata": {
"azdata_cell_guid": "4fbaf071-55a1-40bc-be7e-7b9b5547b886"
},
"outputs": [],
"execution_count": null
}
}
]
}
}

View File

@@ -18,7 +18,7 @@
"onView:azureArc"
],
"extensionDependencies": [
"Microsoft.azdata",
"Microsoft.azcli",
"Microsoft.resource-deployment"
],
"repository": {
@@ -190,7 +190,7 @@
"editable": false,
"options": {
"source": {
"providerId": "arc.controller.config.profiles",
"providerId": "azcli.arc.controller.config.profiles",
"loadingText": "%arc.data.controller.cluster.config.profile.loading%",
"loadingCompletedText": "%arc.data.controller.cluster.config.profile.loadingcompleted%"
},
@@ -289,6 +289,21 @@
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_STORAGE_CLASS",
"type": "kube_storage_class",
"required": true
},
{
"label": "%arc.data.controller.infrastructure%",
"defaultValue": "azure",
"variableName": "AZDATA_NB_VAR_ARC_INFRASTRUCTURE",
"type": "infrastructure",
"infrastructure": [
"azure",
"gcp",
"aws",
"alibaba",
"onpremises",
"other"
],
"required": true
}
]
},
@@ -507,6 +522,12 @@
"type": "readonly_text",
"isEvaluated": true,
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAME)"
},
{
"label": "%arc.data.controller.summary.data.controller.infrastructure%",
"type": "readonly_text",
"isEvaluated": true,
"defaultValue": "$(AZDATA_NB_VAR_ARC_INFRASTRUCTURE)"
}
]
}
@@ -519,8 +540,7 @@
"name": "kubectl"
},
{
"name": "azdata",
"version": "20.3.4"
"name": "azure-cli"
}
],
"when": true
@@ -573,11 +593,8 @@
"providerId": "arc.controllers",
"variableNames": {
"namespace": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAMESPACE",
"endpoint": "AZDATA_NB_VAR_CONTROLLER_ENDPOINT",
"username": "AZDATA_NB_VAR_CONTROLLER_USERNAME",
"kubeConfig": "AZDATA_NB_VAR_CONTROLLER_KUBECONFIG",
"clusterContext": "AZDATA_NB_VAR_CONTROLLER_KUBECTL_CONTEXT",
"password": "AZDATA_NB_VAR_CONTROLLER_PASSWORD"
"clusterContext": "AZDATA_NB_VAR_CONTROLLER_KUBECTL_CONTEXT"
}
},
"optionsType": "dropdown"
@@ -838,8 +855,7 @@
"name": "kubectl"
},
{
"name": "azdata",
"version": "20.3.4"
"name": "azure-cli"
}
],
"when": "true"
@@ -915,11 +931,9 @@
"source": {
"providerId": "arc.controllers",
"variableNames": {
"endpoint": "AZDATA_NB_VAR_CONTROLLER_ENDPOINT",
"username": "AZDATA_NB_VAR_CONTROLLER_USERNAME",
"namespace": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAMESPACE",
"kubeConfig": "AZDATA_NB_VAR_CONTROLLER_KUBECONFIG",
"clusterContext": "AZDATA_NB_VAR_CONTROLLER_KUBECTL_CONTEXT",
"password": "AZDATA_NB_VAR_CONTROLLER_PASSWORD"
"clusterContext": "AZDATA_NB_VAR_CONTROLLER_KUBECTL_CONTEXT"
}
},
"optionsType": "dropdown"
@@ -1067,8 +1081,7 @@
"name": "kubectl"
},
{
"name": "azdata",
"version": "20.3.4"
"name": "azure-cli"
}
],
"when": "mi-type=arc-mi"

View File

@@ -33,6 +33,7 @@
"arc.data.controller.name": "Data controller name",
"arc.data.controller.name.validation.description": "Name must consist of lower case alphanumeric characters, '-' or '.', start/end with an alphanumeric character and be 253 characters or less in length.",
"arc.data.controller.location": "Location",
"arc.data.controller.infrastructure": "Infrastructure",
"arc.data.controller.admin.account.title": "Administrator account",
"arc.data.controller.admin.account.name": "Data controller login",
"arc.data.controller.admin.account.password": "Password",
@@ -58,6 +59,7 @@
"arc.data.controller.summary.resource.group": "Resource group",
"arc.data.controller.summary.data.controller.name": "Data controller name",
"arc.data.controller.summary.data.controller.namespace": "Data controller namespace",
"arc.data.controller.summary.data.controller.infrastructure": "Data controller infrastructure",
"arc.data.controller.summary.controller": "Controller",
"arc.data.controller.summary.location": "Location",
"arc.data.controller.agreement": "I accept {0} and {1}.",
@@ -153,7 +155,7 @@
"cores.limit.greater.than.or.equal.to.requested.cores": "Cores limit must be greater than or equal to requested cores",
"requested.memory.less.than.or.equal.to.memory.limit": "Requested memory must be less than or equal to memory limit",
"memory.limit.greater.than.or.equal.to.requested.memory": "Memory limit must be greater than or equal to requested memory",
"arc.agreement.sql.help.text": "Azure Arc enabled Managed Instance provides SQL Server access and feature compatibility that can be deployed on the infrastructure of your choice. While this service is in preview, it has some feature limitations compared to SQL Managed Instance on Azure. {0}",
"arc.agreement.sql.help.text": "Azure Arc enabled Managed Instance provides SQL Server access and feature compatibility that can be deployed on the infrastructure of your choice. {0}",
"arc.agreement.sql.help.text.learn.more": "Learn more",
"arc.agreement.sql.help.text.learn.more.ariaLabel": "Learn more about Azure Arc enabled Managed Instance"
}

View File

@@ -5,8 +5,6 @@
import * as arc from 'arc';
import * as rd from 'resource-deployment';
import * as loc from '../localizedConstants';
import { PasswordToControllerDialog } from '../ui/dialogs/connectControllerDialog';
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
import { ControllerTreeNode } from '../ui/tree/controllerTreeNode';
@@ -17,26 +15,10 @@ export class UserCancelledError extends Error implements rd.ErrorWithType {
}
export function arcApi(treeDataProvider: AzureArcTreeDataProvider): arc.IExtension {
return {
getRegisteredDataControllers: () => getRegisteredDataControllers(treeDataProvider),
getControllerPassword: (controllerInfo: arc.ControllerInfo) => getControllerPassword(treeDataProvider, controllerInfo),
reacquireControllerPassword: (controllerInfo: arc.ControllerInfo) => reacquireControllerPassword(treeDataProvider, controllerInfo)
getRegisteredDataControllers: () => getRegisteredDataControllers(treeDataProvider)
};
}
export async function reacquireControllerPassword(treeDataProvider: AzureArcTreeDataProvider, controllerInfo: arc.ControllerInfo): Promise<string> {
const dialog = new PasswordToControllerDialog(treeDataProvider);
dialog.showDialog(controllerInfo);
const model = await dialog.waitForClose();
if (!model) {
throw new UserCancelledError(loc.userCancelledError);
}
return model.password;
}
export async function getControllerPassword(treeDataProvider: AzureArcTreeDataProvider, controllerInfo: arc.ControllerInfo): Promise<string> {
return await treeDataProvider.getPassword(controllerInfo);
}
export async function getRegisteredDataControllers(treeDataProvider: AzureArcTreeDataProvider): Promise<arc.DataController[]> {
return (await treeDataProvider.getChildren())
.filter(node => node instanceof ControllerTreeNode)

View File

@@ -32,7 +32,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<arc.IE
dialog.showDialog();
const model = await dialog.waitForClose();
if (model) {
await treeDataProvider.addOrUpdateController(model.controllerModel, model.password);
await treeDataProvider.addOrUpdateController(model.controllerModel);
}
});
@@ -50,10 +50,10 @@ export async function activate(context: vscode.ExtensionContext): Promise<arc.IE
vscode.commands.registerCommand('arc.editConnection', async (treeNode: ControllerTreeNode) => {
const dialog = new ConnectToControllerDialog(treeDataProvider);
dialog.showDialog(treeNode.model.info, await treeDataProvider.getPassword(treeNode.model.info));
dialog.showDialog(treeNode.model.info);
const model = await dialog.waitForClose();
if (model) {
await treeDataProvider.addOrUpdateController(model.controllerModel, model.password, true);
await treeDataProvider.addOrUpdateController(model.controllerModel, true);
}
});

View File

@@ -61,6 +61,8 @@ export const controllerEndpoint = localize('arc.controllerEndpoint', "Controller
export const extensionName = localize('arc.extensionName', "Extension name");
export const extensionsDescription = localize('arc.extensionsDescription', "PostgreSQL provides the ability to extend the functionality of your database by using extensions. Extensions allow for bundling multiple related SQL objects together in a single package that can be loaded or removed from your database with a single command. After being loaded in the database, extensions can function like built-in features.");
export const extensionsFunction = localize('arc.extensionsFunction', "Some extensions must be loaded into PostgreSQL at startup time before they can be used. These preloaded extensions can be viewed and edited below.");
export function extensionsAddFunction(extensions: string): string { return localize('arc.extensionsAddFunction', "Some extensions must be loaded into PostgreSQL at startup time before they can be used. To edit, type in comma separated list of valid extensions: ({0}).", extensions); }
export function extensionsAddErrorrMessage(extensions: string): string { return localize('arc.extensionsAddErrorrMessage', "Value should be either of the following: ({0}).", extensions); }
export const extensionsLearnMore = localize('arc.extensionsLearnMore', "Learn more about PostgreSQL extensions.");
export const extensionsTableLoading = localize('arc.extensionsTableLoading', "Table of preloaded extensions are loading.");
export const extensionsTableLabel = localize('arc.extensionsTableLabel', "Table of preloaded extensions.");

View File

@@ -4,12 +4,9 @@
*--------------------------------------------------------------------------------------------*/
import { ControllerInfo, ResourceType } from 'arc';
import * as azdataExt from 'azdata-ext';
import * as azExt from 'az-ext';
import * as vscode from 'vscode';
import { UserCancelledError } from '../common/api';
import { getCurrentClusterContext, getKubeConfigClusterContexts } from '../common/kubeUtils';
import * as loc from '../localizedConstants';
import { ConnectToControllerDialog } from '../ui/dialogs/connectControllerDialog';
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
export type Registration = {
@@ -19,13 +16,13 @@ export type Registration = {
};
export class ControllerModel {
private readonly _azdataApi: azdataExt.IExtension;
private _endpoints: azdataExt.DcEndpointListResult[] = [];
private readonly _azApi: azExt.IExtension;
private _endpoints: azExt.DcEndpointListResult[] = [];
private _registrations: Registration[] = [];
private _controllerConfig: azdataExt.DcConfigShowResult | undefined = undefined;
private _controllerConfig: azExt.DcConfigShowResult | undefined = undefined;
private readonly _onConfigUpdated = new vscode.EventEmitter<azdataExt.DcConfigShowResult | undefined>();
private readonly _onEndpointsUpdated = new vscode.EventEmitter<azdataExt.DcEndpointListResult[]>();
private readonly _onConfigUpdated = new vscode.EventEmitter<azExt.DcConfigShowResult | undefined>();
private readonly _onEndpointsUpdated = new vscode.EventEmitter<azExt.DcEndpointListResult[]>();
private readonly _onRegistrationsUpdated = new vscode.EventEmitter<Registration[]>();
private readonly _onInfoUpdated = new vscode.EventEmitter<ControllerInfo>();
@@ -38,85 +35,26 @@ export class ControllerModel {
public endpointsLastUpdated?: Date;
public registrationsLastUpdated?: Date;
constructor(public treeDataProvider: AzureArcTreeDataProvider, private _info: ControllerInfo, private _password?: string) {
this._azdataApi = <azdataExt.IExtension>vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
constructor(public treeDataProvider: AzureArcTreeDataProvider, private _info: ControllerInfo) {
this._azApi = <azExt.IExtension>vscode.extensions.getExtension(azExt.extension.name)?.exports;
}
public get info(): ControllerInfo {
return this._info;
}
/**
* Gets the controller context to use when executing azdata commands. This is in one of two forms :
*
* If no URL is specified for this controller then just the namespace is used (e.g. test-namespace)
* If a URL is specified then a 3-part name is used, combining the namespace, username and URL separated by
* / (e.g. test-namespace/admin/https://10.91.86.13:30080)
*/
public get controllerContext(): string {
if (this._info.endpoint) {
return `${this._info.namespace}/${this._info.username}/${this._info.endpoint}`;
}
return this._info.namespace;
}
public set info(value: ControllerInfo) {
this._info = value;
this._onInfoUpdated.fire(this._info);
}
public get azdataAdditionalEnvVars(): azdataExt.AdditionalEnvVars {
public get azAdditionalEnvVars(): azExt.AdditionalEnvVars {
return {
'KUBECONFIG': this.info.kubeConfigFilePath,
'KUBECTL_CONTEXT': this.info.kubeClusterContext
};
}
/**
* Calls azdata login to set the context to this controller and acquires a login session to prevent other
* calls from changing the context while commands for this session are being executed.
* @param promptReconnect
*/
public async login(promptReconnect: boolean = false): Promise<void> {
let promptForValidClusterContext: boolean = false;
try {
const contexts = getKubeConfigClusterContexts(this.info.kubeConfigFilePath);
getCurrentClusterContext(contexts, this.info.kubeClusterContext, true); // this throws if this.info.kubeClusterContext is not found in 'contexts'
} catch (error) {
const response = await vscode.window.showErrorMessage(loc.clusterContextConfigNoLongerValid(this.info.kubeConfigFilePath, this.info.kubeClusterContext, error), loc.yes, loc.no);
if (response === loc.yes) {
promptForValidClusterContext = true;
} else {
if (!promptReconnect) { //throw unless we are required to prompt for reconnect anyways
throw error;
}
}
}
// We haven't gotten our password yet or we want to prompt for a reconnect or we want to prompt to reacquire valid cluster context or any and all of these.
if (!this._password || promptReconnect || promptForValidClusterContext) {
this._password = '';
if (this.info.rememberPassword) {
// It should be in the credentials store, get it from there
this._password = await this.treeDataProvider.getPassword(this.info);
}
if (promptReconnect || !this._password || promptForValidClusterContext) {
// No password yet or we want to re-prompt for credentials so prompt for it from the user
const dialog = new ConnectToControllerDialog(this.treeDataProvider);
dialog.showDialog(this.info, this._password);
const model = await dialog.waitForClose();
if (model) {
await this.treeDataProvider.addOrUpdateController(model.controllerModel, model.password, false);
this._password = model.password;
this._info = model.controllerModel.info;
} else {
throw new UserCancelledError(loc.userCancelledError);
}
}
}
await this._azdataApi.azdata.login({ endpoint: this.info.endpoint, namespace: this.info.namespace }, this.info.username, this._password, this.azdataAdditionalEnvVars);
}
/**
* Refreshes the Tree Node for this model. This will also result in the model being refreshed.
*/
@@ -125,16 +63,14 @@ export class ControllerModel {
if (node) {
this.treeDataProvider.refreshNode(node);
} else {
await this.refresh(false);
await this.refresh(false, this.info.namespace);
}
}
public async refresh(showErrors: boolean = true): Promise<void> {
// First need to log in to ensure that we're able to authenticate with the controller
await this.login(false);
public async refresh(showErrors: boolean = true, namespace: string): Promise<void> {
const newRegistrations: Registration[] = [];
await Promise.all([
this._azdataApi.azdata.arc.dc.config.show(this.azdataAdditionalEnvVars, this.controllerContext).then(result => {
this._controllerConfig = result.result;
this._azApi.az.arcdata.dc.config.show(namespace, this.azAdditionalEnvVars).then(result => {
this._controllerConfig = result.stdout;
this.configLastUpdated = new Date();
this._onConfigUpdated.fire(this._controllerConfig);
}).catch(err => {
@@ -147,8 +83,8 @@ export class ControllerModel {
this._onConfigUpdated.fire(this._controllerConfig);
throw err;
}),
this._azdataApi.azdata.arc.dc.endpoint.list(this.azdataAdditionalEnvVars, this.controllerContext).then(result => {
this._endpoints = result.result;
this._azApi.az.arcdata.dc.endpoint.list(namespace, this.azAdditionalEnvVars).then(result => {
this._endpoints = result.stdout;
this.endpointsLastUpdated = new Date();
this._onEndpointsUpdated.fire(this._endpoints);
}).catch(err => {
@@ -162,8 +98,8 @@ export class ControllerModel {
throw err;
}),
Promise.all([
this._azdataApi.azdata.arc.postgres.server.list(this.azdataAdditionalEnvVars, this.controllerContext).then(result => {
newRegistrations.push(...result.result.map(r => {
this._azApi.az.postgres.arcserver.list(namespace, this.azAdditionalEnvVars).then(result => {
newRegistrations.push(...result.stdout.map(r => {
return {
instanceName: r.name,
state: r.state,
@@ -171,14 +107,15 @@ export class ControllerModel {
};
}));
}),
this._azdataApi.azdata.arc.sql.mi.list(this.azdataAdditionalEnvVars, this.controllerContext).then(result => {
newRegistrations.push(...result.result.map(r => {
this._azApi.az.sql.miarc.list(namespace, this.azAdditionalEnvVars).then(result => {
newRegistrations.push(...result.stdout.map(r => {
return {
instanceName: r.name,
state: r.state,
instanceType: ResourceType.sqlManagedInstances
};
}));
})
]).then(() => {
this._registrations = newRegistrations;
@@ -188,11 +125,11 @@ export class ControllerModel {
]);
}
public get endpoints(): azdataExt.DcEndpointListResult[] {
public get endpoints(): azExt.DcEndpointListResult[] {
return this._endpoints;
}
public getEndpoint(name: string): azdataExt.DcEndpointListResult | undefined {
public getEndpoint(name: string): azExt.DcEndpointListResult | undefined {
return this._endpoints.find(e => e.name === name);
}
@@ -200,7 +137,7 @@ export class ControllerModel {
return this._registrations;
}
public get controllerConfig(): azdataExt.DcConfigShowResult | undefined {
public get controllerConfig(): azExt.DcConfigShowResult | undefined {
return this._controllerConfig;
}
@@ -214,6 +151,6 @@ export class ControllerModel {
* property to for use a display label for this controller
*/
public get label(): string {
return `${this.info.name} (${this.controllerContext})`;
return `${this.info.name}`;
}
}

View File

@@ -5,7 +5,7 @@
import { MiaaResourceInfo } from 'arc';
import * as azdata from 'azdata';
import * as azdataExt from 'azdata-ext';
import * as azExt from 'az-ext';
import * as vscode from 'vscode';
import { UserCancelledError } from '../common/api';
import { Deferred } from '../common/promise';
@@ -20,12 +20,12 @@ export type DatabaseModel = { name: string, status: string };
export class MiaaModel extends ResourceModel {
private _config: azdataExt.SqlMiShowResult | undefined;
private _config: azExt.SqlMiShowResult | undefined;
private _databases: DatabaseModel[] = [];
private readonly _onConfigUpdated = new vscode.EventEmitter<azdataExt.SqlMiShowResult | undefined>();
private readonly _onConfigUpdated = new vscode.EventEmitter<azExt.SqlMiShowResult | undefined>();
private readonly _onDatabasesUpdated = new vscode.EventEmitter<DatabaseModel[]>();
private readonly _azdataApi: azdataExt.IExtension;
private readonly _azApi: azExt.IExtension;
public onConfigUpdated = this._onConfigUpdated.event;
public onDatabasesUpdated = this._onDatabasesUpdated.event;
public configLastUpdated: Date | undefined;
@@ -35,7 +35,7 @@ export class MiaaModel extends ResourceModel {
constructor(_controllerModel: ControllerModel, private _miaaInfo: MiaaResourceInfo, registration: Registration, private _treeDataProvider: AzureArcTreeDataProvider) {
super(_controllerModel, _miaaInfo, registration);
this._azdataApi = <azdataExt.IExtension>vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
this._azApi = <azExt.IExtension>vscode.extensions.getExtension(azExt.extension.name)?.exports;
}
/**
@@ -48,7 +48,7 @@ export class MiaaModel extends ResourceModel {
/**
* The status of this instance
*/
public get config(): azdataExt.SqlMiShowResult | undefined {
public get config(): azExt.SqlMiShowResult | undefined {
return this._config;
}
@@ -73,8 +73,8 @@ export class MiaaModel extends ResourceModel {
this._refreshPromise = new Deferred();
try {
try {
const result = await this._azdataApi.azdata.arc.sql.mi.show(this.info.name, this.controllerModel.azdataAdditionalEnvVars, this.controllerModel.controllerContext);
this._config = result.result;
const result = await this._azApi.az.sql.miarc.show(this.info.name, this.controllerModel.info.namespace, this.controllerModel.azAdditionalEnvVars);
this._config = result.stdout;
this.configLastUpdated = new Date();
this._onConfigUpdated.fire(this._config);
} catch (err) {

View File

@@ -5,7 +5,7 @@
import { PGResourceInfo } from 'arc';
import * as azdata from 'azdata';
import * as azdataExt from 'azdata-ext';
import * as azExt from 'az-ext';
import * as vscode from 'vscode';
import * as loc from '../localizedConstants';
import { ConnectToPGSqlDialog } from '../ui/dialogs/connectPGDialog';
@@ -27,12 +27,12 @@ export type EngineSettingsModel = {
};
export class PostgresModel extends ResourceModel {
private _config?: azdataExt.PostgresServerShowResult;
private _config?: azExt.PostgresServerShowResult;
public workerNodesEngineSettings: EngineSettingsModel[] = [];
public coordinatorNodeEngineSettings: EngineSettingsModel[] = [];
private readonly _azdataApi: azdataExt.IExtension;
private readonly _azApi: azExt.IExtension;
private readonly _onConfigUpdated = new vscode.EventEmitter<azdataExt.PostgresServerShowResult>();
private readonly _onConfigUpdated = new vscode.EventEmitter<azExt.PostgresServerShowResult>();
public onConfigUpdated = this._onConfigUpdated.event;
public configLastUpdated?: Date;
public engineSettingsLastUpdated?: Date;
@@ -42,11 +42,11 @@ export class PostgresModel extends ResourceModel {
constructor(_controllerModel: ControllerModel, private _pgInfo: PGResourceInfo, registration: Registration, private _treeDataProvider: AzureArcTreeDataProvider) {
super(_controllerModel, _pgInfo, registration);
this._azdataApi = <azdataExt.IExtension>vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
this._azApi = <azExt.IExtension>vscode.extensions.getExtension(azExt.extension.name)?.exports;
}
/** Returns the configuration of Postgres */
public get config(): azdataExt.PostgresServerShowResult | undefined {
public get config(): azExt.PostgresServerShowResult | undefined {
return this._config;
}
@@ -118,7 +118,7 @@ export class PostgresModel extends ResourceModel {
}
this._refreshPromise = new Deferred();
try {
this._config = (await this._azdataApi.azdata.arc.postgres.server.show(this.info.name, this.controllerModel.azdataAdditionalEnvVars, this.controllerModel.controllerContext)).result;
this._config = (await this._azApi.az.postgres.arcserver.show(this.info.name, this.controllerModel.info.namespace, this.controllerModel.azAdditionalEnvVars)).stdout;
this.configLastUpdated = new Date();
this._onConfigUpdated.fire(this._config);
this._refreshPromise.resolve();

View File

@@ -3,10 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as arc from 'arc';
import * as azdata from 'azdata';
import * as rd from 'resource-deployment';
import { getControllerPassword, getRegisteredDataControllers, reacquireControllerPassword } from '../common/api';
import { getRegisteredDataControllers } from '../common/api';
import { throwUnless } from '../common/utils';
import * as loc from '../localizedConstants';
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
@@ -31,32 +30,17 @@ export class ArcControllersOptionsSourceProvider implements rd.IOptionsSourcePro
throwUnless(controller !== undefined, loc.noControllerInfoFound(controllerLabel));
switch (variableName) {
case 'namespace': return controller.info.namespace || '';
case 'endpoint': return controller.info.endpoint || '';
case 'username': return controller.info.username;
case 'kubeConfig': return controller.info.kubeConfigFilePath;
case 'clusterContext': return controller.info.kubeClusterContext;
case 'password': return this.getPassword(controller);
default: throw new Error(loc.variableValueFetchForUnsupportedVariable(variableName));
}
}
private async getPassword(controller: arc.DataController): Promise<string> {
let password = await getControllerPassword(this._treeProvider, controller.info);
if (!password) {
password = await reacquireControllerPassword(this._treeProvider, controller.info);
}
throwUnless(password !== undefined, loc.noPasswordFound(controller.label));
return password;
}
public getIsPassword(variableName: string): boolean {
switch (variableName) {
case 'namespace': return false;
case 'endpoint': return false;
case 'username': return false;
case 'kubeConfig': return false;
case 'clusterContext': return false;
case 'password': return true;
default: throw new Error(loc.isPasswordFetchForUnsupportedVariable(variableName));
}
}

View File

@@ -1,97 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdataExt from 'azdata-ext';
/**
* Simple fake Azdata Api used to mock the API during tests
*/
export class FakeAzdataApi implements azdataExt.IAzdataApi {
private _arcApi = {
dc: {
create(_namespace: string, _name: string, _connectivityMode: string, _resourceGroup: string, _location: string, _subscription: string, _profileName?: string, _storageClass?: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
endpoint: {
async list(): Promise<azdataExt.AzdataOutput<azdataExt.DcEndpointListResult[]>> { return <any>{ result: [] }; }
},
config: {
list(): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigListResult[]>> { throw new Error('Method not implemented.'); },
async show(): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigShowResult>> { return <any>{ result: undefined! }; }
}
},
postgres: {
server: {
postgresInstances: <azdataExt.PostgresServerListResult[]>[],
delete(_name: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
async list(): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerListResult[]>> { return { result: this.postgresInstances, logs: [], stdout: [], stderr: [] }; },
show(_name: string): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerShowResult>> { throw new Error('Method not implemented.'); },
edit(
_name: string,
_args: {
adminPassword?: boolean,
coresLimit?: string,
coresRequest?: string,
coordinatorEngineSettings?: string,
engineSettings?: string,
extensions?: string,
memoryLimit?: string,
memoryRequest?: string,
noWait?: boolean,
port?: number,
replaceEngineSettings?: boolean,
workerEngineSettings?: string,
workers?: number
},
_additionalEnvVars?: azdataExt.AdditionalEnvVars
): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); }
}
},
sql: {
mi: {
miaaInstances: <azdataExt.SqlMiListResult[]>[],
delete(_name: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
async list(): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiListResult[]>> { return { logs: [], stdout: [], stderr: [], result: this.miaaInstances }; },
show(_name: string): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiShowResult>> { throw new Error('Method not implemented.'); },
edit(
_name: string,
_args: {
coresLimit?: string,
coresRequest?: string,
memoryLimit?: string,
memoryRequest?: string,
noWait?: boolean
}): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); }
}
}
};
public set postgresInstances(instances: azdataExt.PostgresServerListResult[]) {
this._arcApi.postgres.server.postgresInstances = instances;
}
public set miaaInstances(instances: azdataExt.SqlMiListResult[]) {
this._arcApi.sql.mi.miaaInstances = instances;
}
//
// API Implementation
//
public get arc() {
return this._arcApi;
}
getPath(): Promise<string> {
throw new Error('Method not implemented.');
}
login(_endpointOrNamespace: azdataExt.EndpointOrNamespace, _username: string, _password: string, _additionalEnvVars: azdataExt.AdditionalEnvVars = {}, _azdataContext?: string): Promise<azdataExt.AzdataOutput<void>> {
return <any>undefined;
}
version(): Promise<azdataExt.AzdataOutput<string>> {
throw new Error('Method not implemented.');
}
getSemVersion(): any {
throw new Error('Method not implemented.');
}
}

View File

@@ -10,9 +10,9 @@ import { AzureArcTreeDataProvider } from '../../ui/tree/azureArcTreeDataProvider
export class FakeControllerModel extends ControllerModel {
constructor(treeDataProvider?: AzureArcTreeDataProvider, info?: Partial<ControllerInfo>, password?: string) {
constructor(treeDataProvider?: AzureArcTreeDataProvider, info?: Partial<ControllerInfo>) {
const _info: ControllerInfo = Object.assign({ id: uuid(), endpoint: '', kubeConfigFilePath: '', kubeClusterContext: '', name: '', namespace: '', username: '', rememberPassword: false, resources: [] }, info);
super(treeDataProvider!, _info, password);
super(treeDataProvider!, _info);
}
}

View File

@@ -1,221 +1,221 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// /*---------------------------------------------------------------------------------------------
// * Copyright (c) Microsoft Corporation. All rights reserved.
// * Licensed under the Source EULA. See License.txt in the project root for license information.
// *--------------------------------------------------------------------------------------------*/
import { ControllerInfo } from 'arc';
import * as azdata from 'azdata';
import * as azdataExt from 'azdata-ext';
import * as should from 'should';
import * as sinon from 'sinon';
import * as TypeMoq from 'typemoq';
import { v4 as uuid } from 'uuid';
import * as vscode from 'vscode';
import * as loc from '../../localizedConstants';
import * as kubeUtils from '../../common/kubeUtils';
import { UserCancelledError } from '../../common/api';
import { ControllerModel } from '../../models/controllerModel';
import { ConnectToControllerDialog } from '../../ui/dialogs/connectControllerDialog';
import { AzureArcTreeDataProvider } from '../../ui/tree/azureArcTreeDataProvider';
// import { ControllerInfo } from 'arc';
// import * as azdata from 'azdata';
// import * as azExt from 'azdata-ext';
// import * as should from 'should';
// import * as sinon from 'sinon';
// import * as TypeMoq from 'typemoq';
// import { v4 as uuid } from 'uuid';
// import * as vscode from 'vscode';
// import * as loc from '../../localizedConstants';
// import * as kubeUtils from '../../common/kubeUtils';
// import { UserCancelledError } from '../../common/api';
// import { ControllerModel } from '../../models/controllerModel';
// import { ConnectToControllerDialog } from '../../ui/dialogs/connectControllerDialog';
// import { AzureArcTreeDataProvider } from '../../ui/tree/azureArcTreeDataProvider';
interface ExtensionGlobalMemento extends vscode.Memento {
setKeysForSync(keys: string[]): void;
}
// interface ExtensionGlobalMemento extends vscode.Memento {
// setKeysForSync(keys: string[]): void;
// }
function getDefaultControllerInfo(): ControllerInfo {
return {
id: uuid(),
endpoint: '127.0.0.1',
kubeConfigFilePath: '/path/to/.kube/config',
kubeClusterContext: 'currentCluster',
username: 'admin',
name: 'arc',
namespace: 'arc-ns',
rememberPassword: true,
resources: []
};
}
// function getDefaultControllerInfo(): ControllerInfo {
// return {
// id: uuid(),
// endpoint: '127.0.0.1',
// kubeConfigFilePath: '/path/to/.kube/config',
// kubeClusterContext: 'currentCluster',
// username: 'admin',
// name: 'arc',
// namespace: 'arc-ns',
// rememberPassword: true,
// resources: []
// };
// }
describe('ControllerModel', function (): void {
afterEach(function (): void {
sinon.restore();
});
// describe('ControllerModel', function (): void {
// afterEach(function (): void {
// sinon.restore();
// });
describe('azdataLogin', function (): void {
let mockExtensionContext: TypeMoq.IMock<vscode.ExtensionContext>;
let mockGlobalState: TypeMoq.IMock<ExtensionGlobalMemento>;
// describe('azdataLogin', function (): void {
// let mockExtensionContext: TypeMoq.IMock<vscode.ExtensionContext>;
// let mockGlobalState: TypeMoq.IMock<ExtensionGlobalMemento>;
before(function (): void {
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
mockGlobalState = TypeMoq.Mock.ofType<ExtensionGlobalMemento>();
mockExtensionContext.setup(x => x.globalState).returns(() => mockGlobalState.object);
});
// before(function (): void {
// mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
// mockGlobalState = TypeMoq.Mock.ofType<ExtensionGlobalMemento>();
// mockExtensionContext.setup(x => x.globalState).returns(() => mockGlobalState.object);
// });
beforeEach(function (): void {
sinon.stub(ConnectToControllerDialog.prototype, 'showDialog');
sinon.stub(kubeUtils, 'getKubeConfigClusterContexts').returns([{ name: 'currentCluster', isCurrentContext: true }]);
sinon.stub(vscode.window, 'showErrorMessage').resolves(<any>loc.yes);
});
// beforeEach(function (): void {
// sinon.stub(ConnectToControllerDialog.prototype, 'showDialog');
// sinon.stub(kubeUtils, 'getKubeConfigClusterContexts').returns([{ name: 'currentCluster', isCurrentContext: true }]);
// sinon.stub(vscode.window, 'showErrorMessage').resolves(<any>loc.yes);
// });
it('Rejected with expected error when user cancels', async function (): Promise<void> {
// Returning an undefined model here indicates that the dialog closed without clicking "Ok" - usually through the user clicking "Cancel"
sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve(undefined));
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo());
await should(model.login()).be.rejectedWith(new UserCancelledError(loc.userCancelledError));
});
// it('Rejected with expected error when user cancels', async function (): Promise<void> {
// // Returning an undefined model here indicates that the dialog closed without clicking "Ok" - usually through the user clicking "Cancel"
// sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve(undefined));
// const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo());
// await should(model.login()).be.rejectedWith(new UserCancelledError(loc.userCancelledError));
// });
it('Reads password from cred store', async function (): Promise<void> {
const password = 'password123'; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Test password, not actually used")]
// it('Reads password from cred store', async function (): Promise<void> {
// const password = 'password123'; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Test password, not actually used")]
// Set up cred store to return our password
const credProviderMock = TypeMoq.Mock.ofType<azdata.CredentialProvider>();
credProviderMock.setup(x => x.readCredential(TypeMoq.It.isAny())).returns(() => Promise.resolve({ credentialId: 'id', password: password }));
// Need to setup then when Promise.resolving a mocked object : https://github.com/florinn/typemoq/issues/66
credProviderMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.credentials, 'getProvider').returns(Promise.resolve(credProviderMock.object));
// // Set up cred store to return our password
// const credProviderMock = TypeMoq.Mock.ofType<azdata.CredentialProvider>();
// credProviderMock.setup(x => x.readCredential(TypeMoq.It.isAny())).returns(() => Promise.resolve({ credentialId: 'id', password: password }));
// // Need to setup then when Promise.resolving a mocked object : https://github.com/florinn/typemoq/issues/66
// credProviderMock.setup((x: any) => x.then).returns(() => undefined);
// sinon.stub(azdata.credentials, 'getProvider').returns(Promise.resolve(credProviderMock.object));
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo());
// const azExtApiMock = TypeMoq.Mock.ofType<azExt.IExtension>();
// const azdataMock = TypeMoq.Mock.ofType<azExt.IAzdataApi>();
// azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
// azExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
// sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azExtApiMock.object });
// const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo());
await model.login();
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
});
// await model.login();
// azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
// });
it('Prompt for password when not in cred store', async function (): Promise<void> {
const password = 'password123'; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Stub value for testing")]
// it('Prompt for password when not in cred store', async function (): Promise<void> {
// const password = 'password123'; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Stub value for testing")]
// Set up cred store to return empty password
const credProviderMock = TypeMoq.Mock.ofType<azdata.CredentialProvider>();
credProviderMock.setup(x => x.readCredential(TypeMoq.It.isAny())).returns(() => Promise.resolve({ credentialId: 'id', password: '' }));
// Need to setup then when Promise.resolving a mocked object : https://github.com/florinn/typemoq/issues/66
credProviderMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.credentials, 'getProvider').returns(Promise.resolve(credProviderMock.object));
// // Set up cred store to return empty password
// const credProviderMock = TypeMoq.Mock.ofType<azdata.CredentialProvider>();
// credProviderMock.setup(x => x.readCredential(TypeMoq.It.isAny())).returns(() => Promise.resolve({ credentialId: 'id', password: '' }));
// // Need to setup then when Promise.resolving a mocked object : https://github.com/florinn/typemoq/issues/66
// credProviderMock.setup((x: any) => x.then).returns(() => undefined);
// sinon.stub(azdata.credentials, 'getProvider').returns(Promise.resolve(credProviderMock.object));
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
// const azExtApiMock = TypeMoq.Mock.ofType<azExt.IExtension>();
// const azdataMock = TypeMoq.Mock.ofType<azExt.IAzdataApi>();
// azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
// azExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
// sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azExtApiMock.object });
// Set up dialog to return new model with our password
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo(), password);
sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password }));
// // Set up dialog to return new model with our password
// const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo(), password);
// sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password }));
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo());
// const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo());
await model.login();
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
});
// await model.login();
// azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
// });
it('Prompt for password when rememberPassword is true but prompt reconnect is true', async function (): Promise<void> {
const password = 'password123'; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Stub value for testing")]
// Set up cred store to return a password to start with
const credProviderMock = TypeMoq.Mock.ofType<azdata.CredentialProvider>();
credProviderMock.setup(x => x.readCredential(TypeMoq.It.isAny())).returns(() => Promise.resolve({ credentialId: 'id', password: 'originalPassword' }));
// Need to setup then when Promise.resolving a mocked object : https://github.com/florinn/typemoq/issues/66
credProviderMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.credentials, 'getProvider').returns(Promise.resolve(credProviderMock.object));
// it('Prompt for password when rememberPassword is true but prompt reconnect is true', async function (): Promise<void> {
// const password = 'password123'; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Stub value for testing")]
// // Set up cred store to return a password to start with
// const credProviderMock = TypeMoq.Mock.ofType<azdata.CredentialProvider>();
// credProviderMock.setup(x => x.readCredential(TypeMoq.It.isAny())).returns(() => Promise.resolve({ credentialId: 'id', password: 'originalPassword' }));
// // Need to setup then when Promise.resolving a mocked object : https://github.com/florinn/typemoq/issues/66
// credProviderMock.setup((x: any) => x.then).returns(() => undefined);
// sinon.stub(azdata.credentials, 'getProvider').returns(Promise.resolve(credProviderMock.object));
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
// const azExtApiMock = TypeMoq.Mock.ofType<azExt.IExtension>();
// const azdataMock = TypeMoq.Mock.ofType<azExt.IAzdataApi>();
// azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
// azExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
// sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azExtApiMock.object });
// Set up dialog to return new model with our new password from the reprompt
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo(), password);
const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password }));
// // Set up dialog to return new model with our new password from the reprompt
// const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo(), password);
// const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password }));
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo());
// const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo());
await model.login(true);
should(waitForCloseStub.called).be.true('waitForClose should have been called');
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
});
// await model.login(true);
// should(waitForCloseStub.called).be.true('waitForClose should have been called');
// azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
// });
it('Prompt for password when we already have a password but prompt reconnect is true', async function (): Promise<void> {
const password = 'password123'; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Stub value for testing")]
// Set up cred store to return a password to start with
const credProviderMock = TypeMoq.Mock.ofType<azdata.CredentialProvider>();
credProviderMock.setup(x => x.readCredential(TypeMoq.It.isAny())).returns(() => Promise.resolve({ credentialId: 'id', password: 'originalPassword' }));
// Need to setup then when Promise.resolving a mocked object : https://github.com/florinn/typemoq/issues/66
credProviderMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.credentials, 'getProvider').returns(Promise.resolve(credProviderMock.object));
// it('Prompt for password when we already have a password but prompt reconnect is true', async function (): Promise<void> {
// const password = 'password123'; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Stub value for testing")]
// // Set up cred store to return a password to start with
// const credProviderMock = TypeMoq.Mock.ofType<azdata.CredentialProvider>();
// credProviderMock.setup(x => x.readCredential(TypeMoq.It.isAny())).returns(() => Promise.resolve({ credentialId: 'id', password: 'originalPassword' }));
// // Need to setup then when Promise.resolving a mocked object : https://github.com/florinn/typemoq/issues/66
// credProviderMock.setup((x: any) => x.then).returns(() => undefined);
// sinon.stub(azdata.credentials, 'getProvider').returns(Promise.resolve(credProviderMock.object));
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
// const azExtApiMock = TypeMoq.Mock.ofType<azExt.IExtension>();
// const azdataMock = TypeMoq.Mock.ofType<azExt.IAzdataApi>();
// azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
// azExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
// sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azExtApiMock.object });
// Set up dialog to return new model with our new password from the reprompt
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo(), password);
const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password }));
// // Set up dialog to return new model with our new password from the reprompt
// const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo(), password);
// const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password }));
// Set up original model with a password
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo(), 'originalPassword');
// // Set up original model with a password
// const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo(), 'originalPassword');
await model.login(true);
should(waitForCloseStub.called).be.true('waitForClose should have been called');
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
});
// await model.login(true);
// should(waitForCloseStub.called).be.true('waitForClose should have been called');
// azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
// });
it('Model values are updated correctly when modified during reconnect', async function (): Promise<void> {
const treeDataProvider = new AzureArcTreeDataProvider(mockExtensionContext.object);
// it('Model values are updated correctly when modified during reconnect', async function (): Promise<void> {
// const treeDataProvider = new AzureArcTreeDataProvider(mockExtensionContext.object);
// Set up cred store to return a password to start with
const credProviderMock = TypeMoq.Mock.ofType<azdata.CredentialProvider>();
credProviderMock.setup(x => x.readCredential(TypeMoq.It.isAny())).returns(() => Promise.resolve({ credentialId: 'id', password: 'originalPassword' }));
// Need to setup then when Promise.resolving a mocked object : https://github.com/florinn/typemoq/issues/66
credProviderMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.credentials, 'getProvider').returns(Promise.resolve(credProviderMock.object));
// // Set up cred store to return a password to start with
// const credProviderMock = TypeMoq.Mock.ofType<azdata.CredentialProvider>();
// credProviderMock.setup(x => x.readCredential(TypeMoq.It.isAny())).returns(() => Promise.resolve({ credentialId: 'id', password: 'originalPassword' }));
// // Need to setup then when Promise.resolving a mocked object : https://github.com/florinn/typemoq/issues/66
// credProviderMock.setup((x: any) => x.then).returns(() => undefined);
// sinon.stub(azdata.credentials, 'getProvider').returns(Promise.resolve(credProviderMock.object));
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
// const azExtApiMock = TypeMoq.Mock.ofType<azExt.IExtension>();
// const azdataMock = TypeMoq.Mock.ofType<azExt.IAzdataApi>();
// azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
// azExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
// sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azExtApiMock.object });
// Add existing model to provider
const originalPassword = 'originalPassword';
const model = new ControllerModel(
treeDataProvider,
getDefaultControllerInfo(),
originalPassword
);
await treeDataProvider.addOrUpdateController(model, originalPassword);
// // Add existing model to provider
// const originalPassword = 'originalPassword';
// const model = new ControllerModel(
// treeDataProvider,
// getDefaultControllerInfo(),
// originalPassword
// );
// await treeDataProvider.addOrUpdateController(model, originalPassword);
const newInfo: ControllerInfo = {
id: model.info.id, // The ID stays the same since we're just re-entering information for the same model
endpoint: 'newUrl',
kubeConfigFilePath: '/path/to/.kube/config',
kubeClusterContext: 'currentCluster',
username: 'newUser',
name: 'newName',
namespace: 'newNamespace',
rememberPassword: true,
resources: []
};
const newPassword = 'newPassword';
// Set up dialog to return new model with our new password from the reprompt
const newModel = new ControllerModel(
treeDataProvider,
newInfo,
newPassword);
const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve(
{ controllerModel: newModel, password: newPassword }));
// const newInfo: ControllerInfo = {
// id: model.info.id, // The ID stays the same since we're just re-entering information for the same model
// endpoint: 'newUrl',
// kubeConfigFilePath: '/path/to/.kube/config',
// kubeClusterContext: 'currentCluster',
// username: 'newUser',
// name: 'newName',
// namespace: 'newNamespace',
// rememberPassword: true,
// resources: []
// };
// const newPassword = 'newPassword';
// // Set up dialog to return new model with our new password from the reprompt
// const newModel = new ControllerModel(
// treeDataProvider,
// newInfo,
// newPassword);
// const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve(
// { controllerModel: newModel, password: newPassword }));
await model.login(true);
should(waitForCloseStub.called).be.true('waitForClose should have been called');
should((await treeDataProvider.getChildren()).length).equal(1, 'Tree Data provider should still only have 1 node');
should(model.info).deepEqual(newInfo, 'Model info should have been updated');
// await model.login(true);
// should(waitForCloseStub.called).be.true('waitForClose should have been called');
// should((await treeDataProvider.getChildren()).length).equal(1, 'Tree Data provider should still only have 1 node');
// should(model.info).deepEqual(newInfo, 'Model info should have been updated');
});
// });
});
// });
});
// });

File diff suppressed because it is too large Load Diff

View File

@@ -1,80 +1,77 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// /*---------------------------------------------------------------------------------------------
// * Copyright (c) Microsoft Corporation. All rights reserved.
// * Licensed under the Source EULA. See License.txt in the project root for license information.
// *--------------------------------------------------------------------------------------------*/
import { PGResourceInfo, ResourceType } from 'arc';
import * as azdataExt from 'azdata-ext';
import * as should from 'should';
import * as sinon from 'sinon';
import * as TypeMoq from 'typemoq';
import * as vscode from 'vscode';
import { createModelViewMock } from '@microsoft/azdata-test/out/mocks/modelView/modelViewMock';
import { ControllerModel, Registration } from '../../../models/controllerModel';
import { PostgresModel } from '../../../models/postgresModel';
import { PostgresConnectionStringsPage } from '../../../ui/dashboards/postgres/postgresConnectionStringsPage';
import { AzureArcTreeDataProvider } from '../../../ui/tree/azureArcTreeDataProvider';
import { FakeControllerModel } from '../../mocks/fakeControllerModel';
import { FakeAzdataApi } from '../../mocks/fakeAzdataApi';
import { FakePostgresServerShowOutput } from '../../models/postgresModel.test';
// import { PGResourceInfo, ResourceType } from 'arc';
// import * as azExt from 'azdata-ext';
// import * as should from 'should';
// import * as sinon from 'sinon';
// import * as TypeMoq from 'typemoq';
// import * as vscode from 'vscode';
// import { createModelViewMock } from '@microsoft/azdata-test/out/mocks/modelView/modelViewMock';
// import { ControllerModel, Registration } from '../../../models/controllerModel';
// import { PostgresModel } from '../../../models/postgresModel';
// import { PostgresConnectionStringsPage } from '../../../ui/dashboards/postgres/postgresConnectionStringsPage';
// import { AzureArcTreeDataProvider } from '../../../ui/tree/azureArcTreeDataProvider';
// import { FakeControllerModel } from '../../mocks/fakeControllerModel';
// import { FakeAzdataApi } from '../../mocks/fakeAzdataApi';
// import { FakePostgresServerShowOutput } from '../../models/postgresModel.test';
describe('postgresConnectionStringsPage', function (): void {
let controllerModel: ControllerModel;
let postgresModel: PostgresModel;
let azdataApi: azdataExt.IAzdataApi;
let postgresConnectionStrings: PostgresConnectionStringsPage;
// describe('postgresConnectionStringsPage', function (): void {
// let controllerModel: ControllerModel;
// let postgresModel: PostgresModel;
// let azdataApi: azExt.IAzdataApi;
// let postgresConnectionStrings: PostgresConnectionStringsPage;
afterEach(function (): void {
sinon.restore();
});
// afterEach(function (): void {
// sinon.restore();
// });
beforeEach(async () => {
// Stub the azdata CLI API
azdataApi = new FakeAzdataApi();
const azdataExt = TypeMoq.Mock.ofType<azdataExt.IExtension>();
azdataExt.setup(x => x.azdata).returns(() => azdataApi);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExt.object });
// beforeEach(async () => {
// // Stub the azdata CLI API
// azdataApi = new FakeAzdataApi();
// const azExt = TypeMoq.Mock.ofType<azExt.IExtension>();
// azExt.setup(x => x.azdata).returns(() => azdataApi);
// sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azExt.object });
// Setup Controller Model
controllerModel = new FakeControllerModel();
// // Setup Controller Model
// controllerModel = new FakeControllerModel();
//Stub calling azdata login and acquiring session
sinon.stub(controllerModel, 'login').returns(Promise.resolve());
// // Setup PostgresModel
// const postgresResource: PGResourceInfo = { name: 'pgt', resourceType: '' };
// const registration: Registration = { instanceName: '', state: '', instanceType: ResourceType.postgresInstances };
// postgresModel = new PostgresModel(controllerModel, postgresResource, registration, new AzureArcTreeDataProvider(TypeMoq.Mock.ofType<vscode.ExtensionContext>().object));
// Setup PostgresModel
const postgresResource: PGResourceInfo = { name: 'pgt', resourceType: '' };
const registration: Registration = { instanceName: '', state: '', instanceType: ResourceType.postgresInstances };
postgresModel = new PostgresModel(controllerModel, postgresResource, registration, new AzureArcTreeDataProvider(TypeMoq.Mock.ofType<vscode.ExtensionContext>().object));
// // Setup stub of show call
// const postgresShow = sinon.stub().returns(FakePostgresServerShowOutput);
// sinon.stub(azdataApi, 'arc').get(() => {
// return { postgres: { server: { show(name: string) { return postgresShow(name); } } } };
// });
// Setup stub of show call
const postgresShow = sinon.stub().returns(FakePostgresServerShowOutput);
sinon.stub(azdataApi, 'arc').get(() => {
return { postgres: { server: { show(name: string) { return postgresShow(name); } } } };
});
// // Setup the PostgresConnectionsStringsPage
// let { modelViewMock } = createModelViewMock();
// postgresConnectionStrings = new PostgresConnectionStringsPage(modelViewMock.object, undefined!, postgresModel);
// });
// Setup the PostgresConnectionsStringsPage
let { modelViewMock } = createModelViewMock();
postgresConnectionStrings = new PostgresConnectionStringsPage(modelViewMock.object, undefined!, postgresModel);
});
// describe('getConnectionStrings', function (): void {
describe('getConnectionStrings', function (): void {
// it('Strings container should be empty since postgres model has not been refreshed', async function (): Promise<void> {
// should(postgresConnectionStrings['getConnectionStrings']()).be.empty();
// });
it('Strings container should be empty since postgres model has not been refreshed', async function (): Promise<void> {
should(postgresConnectionStrings['getConnectionStrings']()).be.empty();
});
// it('String contain correct ip and port', async function (): Promise<void> {
// // Call to provide external endpoint
// await postgresModel.refresh();
it('String contain correct ip and port', async function (): Promise<void> {
// Call to provide external endpoint
await postgresModel.refresh();
// let endpoint = FakePostgresServerShowOutput.result.status.primaryEndpoint.split(':');
let endpoint = FakePostgresServerShowOutput.result.status.primaryEndpoint.split(':');
// postgresConnectionStrings['getConnectionStrings']().forEach(k => {
// should(k.value.includes(endpoint[0])).be.True();
// should(k.value.includes(endpoint[1])).be.True();
// });
// });
postgresConnectionStrings['getConnectionStrings']().forEach(k => {
should(k.value.includes(endpoint[0])).be.True();
should(k.value.includes(endpoint[1])).be.True();
});
});
// });
});
});
// });

View File

@@ -1,110 +1,109 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// /*---------------------------------------------------------------------------------------------
// * Copyright (c) Microsoft Corporation. All rights reserved.
// * Licensed under the Source EULA. See License.txt in the project root for license information.
// *--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as sinon from 'sinon';
import * as TypeMoq from 'typemoq';
import * as azdataExt from 'azdata-ext';
import * as utils from '../../../common/utils';
import * as loc from '../../../localizedConstants';
import { Deferred } from '../../../common/promise';
import { createModelViewMock } from '@microsoft/azdata-test/out/mocks/modelView/modelViewMock';
import { StubButton } from '@microsoft/azdata-test/out/stubs/modelView/stubButton';
import { PGResourceInfo, ResourceType } from 'arc';
import { PostgresOverviewPage } from '../../../ui/dashboards/postgres/postgresOverviewPage';
import { AzureArcTreeDataProvider } from '../../../ui/tree/azureArcTreeDataProvider';
import { FakeControllerModel } from '../../mocks/fakeControllerModel';
import { FakeAzdataApi } from '../../mocks/fakeAzdataApi';
import { PostgresModel } from '../../../models/postgresModel';
import { ControllerModel, Registration } from '../../../models/controllerModel';
// import * as vscode from 'vscode';
// import * as sinon from 'sinon';
// import * as TypeMoq from 'typemoq';
// import * as azExt from 'azdata-ext';
// import * as utils from '../../../common/utils';
// import * as loc from '../../../localizedConstants';
// import { Deferred } from '../../../common/promise';
// import { createModelViewMock } from '@microsoft/azdata-test/out/mocks/modelView/modelViewMock';
// import { StubButton } from '@microsoft/azdata-test/out/stubs/modelView/stubButton';
// import { PGResourceInfo, ResourceType } from 'arc';
// import { PostgresOverviewPage } from '../../../ui/dashboards/postgres/postgresOverviewPage';
// import { AzureArcTreeDataProvider } from '../../../ui/tree/azureArcTreeDataProvider';
// import { FakeControllerModel } from '../../mocks/fakeControllerModel';
// import { FakeAzApi } from '../../mocks/fakeAzdataApi';
// import { PostgresModel } from '../../../models/postgresModel';
// import { ControllerModel, Registration } from '../../../models/controllerModel';
describe('postgresOverviewPage', () => {
let postgresOverview: PostgresOverviewPage;
let azdataApi: azdataExt.IAzdataApi;
let controllerModel: ControllerModel;
let postgresModel: PostgresModel;
// describe('postgresOverviewPage', () => {
// let postgresOverview: PostgresOverviewPage;
// let azdataApi: azExt.IAzdataApi;
// let controllerModel: ControllerModel;
// let postgresModel: PostgresModel;
let showInformationMessage: sinon.SinonStub;
let showErrorMessage: sinon.SinonStub;
// let showInformationMessage: sinon.SinonStub;
// let showErrorMessage: sinon.SinonStub;
let informationMessageShown: Deferred;
let errorMessageShown: Deferred;
// let informationMessageShown: Deferred;
// let errorMessageShown: Deferred;
beforeEach(async () => {
// Stub the azdata CLI API
azdataApi = new FakeAzdataApi();
const azdataExt = TypeMoq.Mock.ofType<azdataExt.IExtension>();
azdataExt.setup(x => x.azdata).returns(() => azdataApi);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExt.object });
// beforeEach(async () => {
// // Stub the azdata CLI API
// azdataApi = new FakeAzdataApi();
// const azExt = TypeMoq.Mock.ofType<azExt.IExtension>();
// azExt.setup(x => x.azdata).returns(() => azdataApi);
// sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azExt.object });
// Stub the window UI
informationMessageShown = new Deferred();
showInformationMessage = sinon.stub(vscode.window, 'showInformationMessage').callsFake(
(_: string, __: vscode.MessageOptions, ...___: vscode.MessageItem[]) => {
informationMessageShown.resolve();
return Promise.resolve(undefined);
});
// // Stub the window UI
// informationMessageShown = new Deferred();
// showInformationMessage = sinon.stub(vscode.window, 'showInformationMessage').callsFake(
// (_: string, __: vscode.MessageOptions, ...___: vscode.MessageItem[]) => {
// informationMessageShown.resolve();
// return Promise.resolve(undefined);
// });
errorMessageShown = new Deferred();
showErrorMessage = sinon.stub(vscode.window, 'showErrorMessage').callsFake(
(_: string, __: vscode.MessageOptions, ...___: vscode.MessageItem[]) => {
errorMessageShown.resolve();
return Promise.resolve(undefined);
});
// errorMessageShown = new Deferred();
// showErrorMessage = sinon.stub(vscode.window, 'showErrorMessage').callsFake(
// (_: string, __: vscode.MessageOptions, ...___: vscode.MessageItem[]) => {
// errorMessageShown.resolve();
// return Promise.resolve(undefined);
// });
// Setup the PostgresModel
controllerModel = new FakeControllerModel();
const postgresResource: PGResourceInfo = { name: 'my-pg', resourceType: '' };
const registration: Registration = { instanceName: '', state: '', instanceType: ResourceType.postgresInstances };
const treeDataProvider = new AzureArcTreeDataProvider(TypeMoq.Mock.ofType<vscode.ExtensionContext>().object);
postgresModel = new PostgresModel(controllerModel, postgresResource, registration, treeDataProvider);
// // Setup the PostgresModel
// controllerModel = new FakeControllerModel();
// const postgresResource: PGResourceInfo = { name: 'my-pg', resourceType: '' };
// const registration: Registration = { instanceName: '', state: '', instanceType: ResourceType.postgresInstances };
// const treeDataProvider = new AzureArcTreeDataProvider(TypeMoq.Mock.ofType<vscode.ExtensionContext>().object);
// postgresModel = new PostgresModel(controllerModel, postgresResource, registration, treeDataProvider);
// Setup the PostgresOverviewPage
const { modelViewMock } = createModelViewMock();
postgresOverview = new PostgresOverviewPage(modelViewMock.object, undefined!, controllerModel, postgresModel);
// Call the getter to initialize toolbar, but we don't need to use it for anything
// eslint-disable-next-line code-no-unused-expressions
postgresOverview['toolbarContainer'];
});
// // Setup the PostgresOverviewPage
// const { modelViewMock } = createModelViewMock();
// postgresOverview = new PostgresOverviewPage(modelViewMock.object, undefined!, controllerModel, postgresModel);
// // Call the getter to initialize toolbar, but we don't need to use it for anything
// // eslint-disable-next-line code-no-unused-expressions
// postgresOverview['toolbarContainer'];
// });
afterEach(() => {
sinon.restore();
});
// afterEach(() => {
// sinon.restore();
// });
describe('delete button', () => {
let refreshTreeNode: sinon.SinonStub;
// describe('delete button', () => {
// let refreshTreeNode: sinon.SinonStub;
beforeEach(() => {
sinon.stub(utils, 'promptForInstanceDeletion').returns(Promise.resolve(true));
sinon.stub(controllerModel, 'login').returns(Promise.resolve());
refreshTreeNode = sinon.stub(controllerModel, 'refreshTreeNode');
});
// beforeEach(() => {
// sinon.stub(utils, 'promptForInstanceDeletion').returns(Promise.resolve(true));
// refreshTreeNode = sinon.stub(controllerModel, 'refreshTreeNode');
// });
it('deletes Postgres on success', async () => {
// Stub 'azdata arc postgres server delete' to return success
const postgresDeleteStub = sinon.stub(azdataApi.arc.postgres.server, 'delete');
// it('deletes Postgres on success', async () => {
// // Stub 'azdata arc postgres server delete' to return success
// const postgresDeleteStub = sinon.stub(azdataApi.arc.postgres.server, 'delete');
(postgresOverview['deleteButton'] as StubButton).click();
await informationMessageShown;
sinon.assert.calledOnceWithExactly(postgresDeleteStub, postgresModel.info.name, sinon.match.any, sinon.match.any);
sinon.assert.calledOnceWithExactly(showInformationMessage, loc.instanceDeleted(postgresModel.info.name));
sinon.assert.notCalled(showErrorMessage);
sinon.assert.calledOnce(refreshTreeNode);
});
// (postgresOverview['deleteButton'] as StubButton).click();
// await informationMessageShown;
// sinon.assert.calledOnceWithExactly(postgresDeleteStub, postgresModel.info.name, sinon.match.any, sinon.match.any);
// sinon.assert.calledOnceWithExactly(showInformationMessage, loc.instanceDeleted(postgresModel.info.name));
// sinon.assert.notCalled(showErrorMessage);
// sinon.assert.calledOnce(refreshTreeNode);
// });
it('shows an error message on failure', async () => {
// Stub 'azdata arc postgres server delete' to throw an exception
const error = new Error('something bad happened');
const postgresDeleteStub = sinon.stub(azdataApi.arc.postgres.server, 'delete').throws(error);
// it('shows an error message on failure', async () => {
// // Stub 'azdata arc postgres server delete' to throw an exception
// const error = new Error('something bad happened');
// const postgresDeleteStub = sinon.stub(azdataApi.arc.postgres.server, 'delete').throws(error);
(postgresOverview['deleteButton'] as StubButton).click();
await errorMessageShown;
sinon.assert.calledOnceWithExactly(postgresDeleteStub, postgresModel.info.name, sinon.match.any, sinon.match.any);
sinon.assert.notCalled(showInformationMessage);
sinon.assert.calledOnceWithExactly(showErrorMessage, loc.instanceDeletionFailed(postgresModel.info.name, error.message));
sinon.assert.notCalled(refreshTreeNode);
});
});
});
// (postgresOverview['deleteButton'] as StubButton).click();
// await errorMessageShown;
// sinon.assert.calledOnceWithExactly(postgresDeleteStub, postgresModel.info.name, sinon.match.any, sinon.match.any);
// sinon.assert.notCalled(showInformationMessage);
// sinon.assert.calledOnceWithExactly(showErrorMessage, loc.instanceDeletionFailed(postgresModel.info.name, error.message));
// sinon.assert.notCalled(refreshTreeNode);
// });
// });
// });

View File

@@ -1,97 +1,97 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// /*---------------------------------------------------------------------------------------------
// * Copyright (c) Microsoft Corporation. All rights reserved.
// * Licensed under the Source EULA. See License.txt in the project root for license information.
// *--------------------------------------------------------------------------------------------*/
import { ControllerInfo } from 'arc';
import * as should from 'should';
import * as sinon from 'sinon';
import { v4 as uuid } from 'uuid';
import * as loc from '../../../localizedConstants';
import { ControllerModel } from '../../../models/controllerModel';
import { ConnectToControllerDialog } from '../../../ui/dialogs/connectControllerDialog';
// import { ControllerInfo } from 'arc';
// import * as should from 'should';
// import * as sinon from 'sinon';
// import { v4 as uuid } from 'uuid';
// import * as loc from '../../../localizedConstants';
// import { ControllerModel } from '../../../models/controllerModel';
// import { ConnectToControllerDialog } from '../../../ui/dialogs/connectControllerDialog';
describe('ConnectControllerDialog', function (): void {
afterEach(function (): void {
sinon.restore();
});
// describe('ConnectControllerDialog', function (): void {
// afterEach(function (): void {
// sinon.restore();
// });
(<{ info: ControllerInfo | undefined, description: string }[]>[
{ info: undefined, description: 'all input' },
{ info: { endpoint: '127.0.0.1' }, description: 'all but URL' },
{ info: { endpoint: '127.0.0.1', username: 'sa' }, description: 'all but URL and password' }]).forEach(test => {
it(`Validate returns false when ${test.description} is empty`, async function (): Promise<void> {
const connectControllerDialog = new ConnectToControllerDialog(undefined!);
connectControllerDialog.showDialog(test.info, undefined);
await connectControllerDialog.isInitialized;
const validateResult = await connectControllerDialog.validate();
should(validateResult).be.false();
});
});
// (<{ info: ControllerInfo | undefined, description: string }[]>[
// { info: undefined, description: 'all input' },
// { info: { endpoint: '127.0.0.1' }, description: 'all but URL' },
// { info: { endpoint: '127.0.0.1', username: 'sa' }, description: 'all but URL and password' }]).forEach(test => {
// it(`Validate returns false when ${test.description} is empty`, async function (): Promise<void> {
// const connectControllerDialog = new ConnectToControllerDialog(undefined!);
// connectControllerDialog.showDialog(test.info, undefined);
// await connectControllerDialog.isInitialized;
// const validateResult = await connectControllerDialog.validate();
// should(validateResult).be.false();
// });
// });
it('validate returns false if controller refresh fails', async function (): Promise<void> {
sinon.stub(ControllerModel.prototype, 'refresh').returns(Promise.reject('Controller refresh failed'));
const connectControllerDialog = new ConnectToControllerDialog(undefined!);
const info: ControllerInfo = { id: uuid(), endpoint: 'https://127.0.0.1:30080', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] };
connectControllerDialog.showDialog(info, 'pwd');
await connectControllerDialog.isInitialized;
const validateResult = await connectControllerDialog.validate();
should(validateResult).be.false('Validation should have returned false');
});
// it('validate returns false if controller refresh fails', async function (): Promise<void> {
// sinon.stub(ControllerModel.prototype, 'refresh').returns(Promise.reject('Controller refresh failed'));
// const connectControllerDialog = new ConnectToControllerDialog(undefined!);
// const info: ControllerInfo = { id: uuid(), endpoint: 'https://127.0.0.1:30080', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] };
// connectControllerDialog.showDialog(info, 'pwd');
// await connectControllerDialog.isInitialized;
// const validateResult = await connectControllerDialog.validate();
// should(validateResult).be.false('Validation should have returned false');
// });
it('validate replaces http with https', async function (): Promise<void> {
await validateConnectControllerDialog(
{ id: uuid(), endpoint: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
'https://127.0.0.1:30081');
});
// it('validate replaces http with https', async function (): Promise<void> {
// await validateConnectControllerDialog(
// { id: uuid(), endpoint: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
// 'https://127.0.0.1:30081');
// });
it('validate appends https if missing', async function (): Promise<void> {
await validateConnectControllerDialog({ id: uuid(), endpoint: '127.0.0.1:30080', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
'https://127.0.0.1:30080');
});
// it('validate appends https if missing', async function (): Promise<void> {
// await validateConnectControllerDialog({ id: uuid(), endpoint: '127.0.0.1:30080', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
// 'https://127.0.0.1:30080');
// });
it('validate appends default port if missing', async function (): Promise<void> {
await validateConnectControllerDialog({ id: uuid(), endpoint: 'https://127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
'https://127.0.0.1:30080');
});
// it('validate appends default port if missing', async function (): Promise<void> {
// await validateConnectControllerDialog({ id: uuid(), endpoint: 'https://127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
// 'https://127.0.0.1:30080');
// });
it('validate appends both port and https if missing', async function (): Promise<void> {
await validateConnectControllerDialog({ id: uuid(), endpoint: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
'https://127.0.0.1:30080');
});
// it('validate appends both port and https if missing', async function (): Promise<void> {
// await validateConnectControllerDialog({ id: uuid(), endpoint: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
// 'https://127.0.0.1:30080');
// });
for (const name of ['', undefined]) {
it.skip(`validate display name gets set to arc instance name for user chosen name of:${name}`, async function (): Promise<void> {
await validateConnectControllerDialog(
{ id: uuid(), endpoint: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: name!, namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
'https://127.0.0.1:30081');
});
}
// for (const name of ['', undefined]) {
// it.skip(`validate display name gets set to arc instance name for user chosen name of:${name}`, async function (): Promise<void> {
// await validateConnectControllerDialog(
// { id: uuid(), endpoint: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: name!, namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
// 'https://127.0.0.1:30081');
// });
// }
it.skip(`validate display name gets set to default data controller name for user chosen name of:'' and instanceName in explicably returned as undefined from the controller endpoint`, async function (): Promise<void> {
await validateConnectControllerDialog(
{ id: uuid(), endpoint: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: '', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
'https://127.0.0.1:30081',
undefined);
});
});
// it.skip(`validate display name gets set to default data controller name for user chosen name of:'' and instanceName in explicably returned as undefined from the controller endpoint`, async function (): Promise<void> {
// await validateConnectControllerDialog(
// { id: uuid(), endpoint: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: '', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
// 'https://127.0.0.1:30081',
// undefined);
// });
// });
async function validateConnectControllerDialog(info: ControllerInfo, expectedUrl: string, arcInstanceName: string = 'arc-instance'): Promise<void> {
const expectedControllerInfoName = info.name || arcInstanceName || loc.defaultControllerName;
const connectControllerDialog = new ConnectToControllerDialog(undefined!);
// Stub out refresh calls to controllerModel - we'll test those separately
sinon.stub(ControllerModel.prototype, 'refresh').returns(Promise.resolve());
// stub out controller registration response to return a known instanceName for the dc.
/*
sinon.stub(ControllerModel.prototype, 'controllerRegistration').get(() => {
return <Registration>{ instanceName: arcInstanceName };
});
*/
connectControllerDialog.showDialog(info, 'pwd');
await connectControllerDialog.isInitialized;
const validateResult = await connectControllerDialog.validate();
should(validateResult).be.true('Validation should have returned true');
const model = await connectControllerDialog.waitForClose();
should(model?.controllerModel.info.endpoint).equal(expectedUrl);
should(model?.controllerModel.info.name).equal(expectedControllerInfoName);
}
// async function validateConnectControllerDialog(info: ControllerInfo, expectedUrl: string, arcInstanceName: string = 'arc-instance'): Promise<void> {
// const expectedControllerInfoName = info.name || arcInstanceName || loc.defaultControllerName;
// const connectControllerDialog = new ConnectToControllerDialog(undefined!);
// // Stub out refresh calls to controllerModel - we'll test those separately
// sinon.stub(ControllerModel.prototype, 'refresh').returns(Promise.resolve());
// // stub out controller registration response to return a known instanceName for the dc.
// /*
// sinon.stub(ControllerModel.prototype, 'controllerRegistration').get(() => {
// return <Registration>{ instanceName: arcInstanceName };
// });
// */
// connectControllerDialog.showDialog(info, 'pwd');
// await connectControllerDialog.isInitialized;
// const validateResult = await connectControllerDialog.validate();
// should(validateResult).be.true('Validation should have returned true');
// const model = await connectControllerDialog.waitForClose();
// should(model?.controllerModel.info.endpoint).equal(expectedUrl);
// should(model?.controllerModel.info.name).equal(expectedControllerInfoName);
// }

View File

@@ -1,184 +1,184 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// /*---------------------------------------------------------------------------------------------
// * Copyright (c) Microsoft Corporation. All rights reserved.
// * Licensed under the Source EULA. See License.txt in the project root for license information.
// *--------------------------------------------------------------------------------------------*/
import { ControllerInfo, ResourceType } from 'arc';
import 'mocha';
import * as should from 'should';
import * as TypeMoq from 'typemoq';
import * as sinon from 'sinon';
import { v4 as uuid } from 'uuid';
import * as vscode from 'vscode';
import * as azdataExt from 'azdata-ext';
import * as kubeUtils from '../../../common/kubeUtils';
import { ControllerModel } from '../../../models/controllerModel';
import { MiaaModel } from '../../../models/miaaModel';
import { AzureArcTreeDataProvider } from '../../../ui/tree/azureArcTreeDataProvider';
import { ControllerTreeNode } from '../../../ui/tree/controllerTreeNode';
import { MiaaTreeNode } from '../../../ui/tree/miaaTreeNode';
import { FakeControllerModel } from '../../mocks/fakeControllerModel';
import { FakeAzdataApi } from '../../mocks/fakeAzdataApi';
// import { ControllerInfo, ResourceType } from 'arc';
// import 'mocha';
// import * as should from 'should';
// import * as TypeMoq from 'typemoq';
// import * as sinon from 'sinon';
// import { v4 as uuid } from 'uuid';
// import * as vscode from 'vscode';
// import * as azdataExt from 'azdata-ext';
// import * as kubeUtils from '../../../common/kubeUtils';
// import { ControllerModel } from '../../../models/controllerModel';
// import { MiaaModel } from '../../../models/miaaModel';
// import { AzureArcTreeDataProvider } from '../../../ui/tree/azureArcTreeDataProvider';
// import { ControllerTreeNode } from '../../../ui/tree/controllerTreeNode';
// import { MiaaTreeNode } from '../../../ui/tree/miaaTreeNode';
// import { FakeControllerModel } from '../../mocks/fakeControllerModel';
// import { FakeAzdataApi } from '../../mocks/fakeAzdataApi';
interface ExtensionGlobalMemento extends vscode.Memento {
setKeysForSync(keys: string[]): void;
}
// interface ExtensionGlobalMemento extends vscode.Memento {
// setKeysForSync(keys: string[]): void;
// }
function getDefaultControllerInfo(): ControllerInfo {
return {
id: uuid(),
endpoint: '127.0.0.1',
kubeConfigFilePath: '/path/to/.kube/config',
kubeClusterContext: 'currentCluster',
username: 'sa',
name: 'my-arc',
namespace: 'arc-ns',
rememberPassword: true,
resources: []
};
}
// function getDefaultControllerInfo(): ControllerInfo {
// return {
// id: uuid(),
// endpoint: '127.0.0.1',
// kubeConfigFilePath: '/path/to/.kube/config',
// kubeClusterContext: 'currentCluster',
// username: 'sa',
// name: 'my-arc',
// namespace: 'arc-ns',
// rememberPassword: true,
// resources: []
// };
// }
describe('AzureArcTreeDataProvider tests', function (): void {
let treeDataProvider: AzureArcTreeDataProvider;
beforeEach(function (): void {
const mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
const mockGlobalState = TypeMoq.Mock.ofType<ExtensionGlobalMemento>();
mockGlobalState.setup(x => x.update(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve());
mockExtensionContext.setup(x => x.globalState).returns(() => mockGlobalState.object);
//treeDataProviderMock = TypeMoq.Mock.ofType<AzureArcTreeDataProvider>();
treeDataProvider = new AzureArcTreeDataProvider(mockExtensionContext.object);
});
// describe('AzureArcTreeDataProvider tests', function (): void {
// let treeDataProvider: AzureArcTreeDataProvider;
// beforeEach(function (): void {
// const mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
// const mockGlobalState = TypeMoq.Mock.ofType<ExtensionGlobalMemento>();
// mockGlobalState.setup(x => x.update(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve());
// mockExtensionContext.setup(x => x.globalState).returns(() => mockGlobalState.object);
// //treeDataProviderMock = TypeMoq.Mock.ofType<AzureArcTreeDataProvider>();
// treeDataProvider = new AzureArcTreeDataProvider(mockExtensionContext.object);
// });
describe('addOrUpdateController', function (): void {
it('Multiple Controllers are added correctly', async function (): Promise<void> {
treeDataProvider['_loading'] = false;
let children = await treeDataProvider.getChildren();
should(children.length).equal(0, 'There initially shouldn\'t be any children');
const controllerModel = new FakeControllerModel();
await treeDataProvider.addOrUpdateController(controllerModel, '');
children = await treeDataProvider.getChildren();
should(children.length).equal(1, 'Controller node should be added correctly');
// describe('addOrUpdateController', function (): void {
// it('Multiple Controllers are added correctly', async function (): Promise<void> {
// treeDataProvider['_loading'] = false;
// let children = await treeDataProvider.getChildren();
// should(children.length).equal(0, 'There initially shouldn\'t be any children');
// const controllerModel = new FakeControllerModel();
// await treeDataProvider.addOrUpdateController(controllerModel, '');
// children = await treeDataProvider.getChildren();
// should(children.length).equal(1, 'Controller node should be added correctly');
// Add a couple more
const controllerModel2 = new FakeControllerModel();
const controllerModel3 = new FakeControllerModel();
await treeDataProvider.addOrUpdateController(controllerModel2, '');
await treeDataProvider.addOrUpdateController(controllerModel3, '');
children = await treeDataProvider.getChildren();
should(children.length).equal(3, 'Additional Controller nodes should be added correctly');
});
// // Add a couple more
// const controllerModel2 = new FakeControllerModel();
// const controllerModel3 = new FakeControllerModel();
// await treeDataProvider.addOrUpdateController(controllerModel2, '');
// await treeDataProvider.addOrUpdateController(controllerModel3, '');
// children = await treeDataProvider.getChildren();
// should(children.length).equal(3, 'Additional Controller nodes should be added correctly');
// });
it('Adding a Controller more than once doesn\'t create duplicates', async function (): Promise<void> {
treeDataProvider['_loading'] = false;
let children = await treeDataProvider.getChildren();
should(children.length).equal(0, 'There initially shouldn\'t be any children');
const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo());
await treeDataProvider.addOrUpdateController(controllerModel, '');
should(children.length).equal(1, 'Controller node should be added correctly');
await treeDataProvider.addOrUpdateController(controllerModel, '');
should(children.length).equal(1, 'Shouldn\'t add duplicate controller node');
});
// it('Adding a Controller more than once doesn\'t create duplicates', async function (): Promise<void> {
// treeDataProvider['_loading'] = false;
// let children = await treeDataProvider.getChildren();
// should(children.length).equal(0, 'There initially shouldn\'t be any children');
// const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo());
// await treeDataProvider.addOrUpdateController(controllerModel, '');
// should(children.length).equal(1, 'Controller node should be added correctly');
// await treeDataProvider.addOrUpdateController(controllerModel, '');
// should(children.length).equal(1, 'Shouldn\'t add duplicate controller node');
// });
it('Updating an existing controller works as expected', async function (): Promise<void> {
treeDataProvider['_loading'] = false;
let children = await treeDataProvider.getChildren();
should(children.length).equal(0, 'There initially shouldn\'t be any children');
const originalInfo: ControllerInfo = getDefaultControllerInfo();
const controllerModel = new ControllerModel(treeDataProvider, originalInfo);
await treeDataProvider.addOrUpdateController(controllerModel, '');
should(children.length).equal(1, 'Controller node should be added correctly');
should((<ControllerTreeNode>children[0]).model.info).deepEqual(originalInfo);
const newInfo: ControllerInfo = { id: originalInfo.id, endpoint: '1.1.1.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'new-name', namespace: 'new-namespace', username: 'admin', rememberPassword: false, resources: [] };
const controllerModel2 = new ControllerModel(treeDataProvider, newInfo);
await treeDataProvider.addOrUpdateController(controllerModel2, '');
should(children.length).equal(1, 'Shouldn\'t add duplicate controller node');
should((<ControllerTreeNode>children[0]).model.info).deepEqual(newInfo);
});
});
// it('Updating an existing controller works as expected', async function (): Promise<void> {
// treeDataProvider['_loading'] = false;
// let children = await treeDataProvider.getChildren();
// should(children.length).equal(0, 'There initially shouldn\'t be any children');
// const originalInfo: ControllerInfo = getDefaultControllerInfo();
// const controllerModel = new ControllerModel(treeDataProvider, originalInfo);
// await treeDataProvider.addOrUpdateController(controllerModel, '');
// should(children.length).equal(1, 'Controller node should be added correctly');
// should((<ControllerTreeNode>children[0]).model.info).deepEqual(originalInfo);
// const newInfo: ControllerInfo = { id: originalInfo.id, endpoint: '1.1.1.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'new-name', namespace: 'new-namespace', username: 'admin', rememberPassword: false, resources: [] };
// const controllerModel2 = new ControllerModel(treeDataProvider, newInfo);
// await treeDataProvider.addOrUpdateController(controllerModel2, '');
// should(children.length).equal(1, 'Shouldn\'t add duplicate controller node');
// should((<ControllerTreeNode>children[0]).model.info).deepEqual(newInfo);
// });
// });
describe('getChildren', function (): void {
it('should return an empty array before loading stored controllers is completed', async function (): Promise<void> {
treeDataProvider['_loading'] = true;
let children = await treeDataProvider.getChildren();
should(children.length).equal(0, 'While loading we should return an empty array');
});
// describe('getChildren', function (): void {
// it('should return an empty array before loading stored controllers is completed', async function (): Promise<void> {
// treeDataProvider['_loading'] = true;
// let children = await treeDataProvider.getChildren();
// should(children.length).equal(0, 'While loading we should return an empty array');
// });
it('should return no children after loading', async function (): Promise<void> {
treeDataProvider['_loading'] = false;
let children = await treeDataProvider.getChildren();
should(children.length).equal(0, 'After loading we should have 0 children');
});
// it('should return no children after loading', async function (): Promise<void> {
// treeDataProvider['_loading'] = false;
// let children = await treeDataProvider.getChildren();
// should(children.length).equal(0, 'After loading we should have 0 children');
// });
it('should return all children of controller after loading', async function (): Promise<void> {
const mockArcExtension = TypeMoq.Mock.ofType<vscode.Extension<any>>();
const mockArcApi = TypeMoq.Mock.ofType<azdataExt.IExtension>();
mockArcExtension.setup(x => x.exports).returns(() => {
return mockArcApi.object;
});
const fakeAzdataApi = new FakeAzdataApi();
const pgInstances = [{ name: 'pg1', state: '', workers: 0 }];
const miaaInstances = [{ name: 'miaa1', state: '', replicas: '', serverEndpoint: '' }];
fakeAzdataApi.postgresInstances = pgInstances;
fakeAzdataApi.miaaInstances = miaaInstances;
mockArcApi.setup(x => x.azdata).returns(() => fakeAzdataApi);
// it('should return all children of controller after loading', async function (): Promise<void> {
// const mockArcExtension = TypeMoq.Mock.ofType<vscode.Extension<any>>();
// const mockArcApi = TypeMoq.Mock.ofType<azdataExt.IExtension>();
// mockArcExtension.setup(x => x.exports).returns(() => {
// return mockArcApi.object;
// });
// const fakeAzdataApi = new FakeAzdataApi();
// const pgInstances = [{ name: 'pg1', state: '', workers: 0 }];
// const miaaInstances = [{ name: 'miaa1', state: '', replicas: '', serverEndpoint: '' }];
// fakeAzdataApi.postgresInstances = pgInstances;
// fakeAzdataApi.miaaInstances = miaaInstances;
// mockArcApi.setup(x => x.azdata).returns(() => fakeAzdataApi);
sinon.stub(vscode.extensions, 'getExtension').returns(mockArcExtension.object);
sinon.stub(kubeUtils, 'getKubeConfigClusterContexts').returns([{ name: 'currentCluster', isCurrentContext: true }]);
const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo(), 'mypassword');
await treeDataProvider.addOrUpdateController(controllerModel, '');
const controllerNode = treeDataProvider.getControllerNode(controllerModel);
const children = await treeDataProvider.getChildren(controllerNode);
should(children.filter(c => c.label === pgInstances[0].name).length).equal(1, 'Should have a Postgres child');
should(children.filter(c => c.label === miaaInstances[0].name).length).equal(1, 'Should have a MIAA child');
should(children.length).equal(2, 'Should have exactly 2 children');
});
});
// sinon.stub(vscode.extensions, 'getExtension').returns(mockArcExtension.object);
// sinon.stub(kubeUtils, 'getKubeConfigClusterContexts').returns([{ name: 'currentCluster', isCurrentContext: true }]);
// const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo(), 'mypassword');
// await treeDataProvider.addOrUpdateController(controllerModel, '');
// const controllerNode = treeDataProvider.getControllerNode(controllerModel);
// const children = await treeDataProvider.getChildren(controllerNode);
// should(children.filter(c => c.label === pgInstances[0].name).length).equal(1, 'Should have a Postgres child');
// should(children.filter(c => c.label === miaaInstances[0].name).length).equal(1, 'Should have a MIAA child');
// should(children.length).equal(2, 'Should have exactly 2 children');
// });
// });
describe('removeController', function (): void {
it('removing a controller should work as expected', async function (): Promise<void> {
treeDataProvider['_loading'] = false;
const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo());
const info2 = getDefaultControllerInfo();
info2.username = 'cloudsa';
const controllerModel2 = new ControllerModel(treeDataProvider, info2);
await treeDataProvider.addOrUpdateController(controllerModel, '');
await treeDataProvider.addOrUpdateController(controllerModel2, '');
const children = <ControllerTreeNode[]>(await treeDataProvider.getChildren());
await treeDataProvider.removeController(children[0]);
should((await treeDataProvider.getChildren()).length).equal(1, 'Node should have been removed');
await treeDataProvider.removeController(children[0]);
should((await treeDataProvider.getChildren()).length).equal(1, 'Removing same node again should do nothing');
await treeDataProvider.removeController(children[1]);
should((await treeDataProvider.getChildren()).length).equal(0, 'Removing other node should work');
await treeDataProvider.removeController(children[1]);
should((await treeDataProvider.getChildren()).length).equal(0, 'Removing other node again should do nothing');
});
});
// describe('removeController', function (): void {
// it('removing a controller should work as expected', async function (): Promise<void> {
// treeDataProvider['_loading'] = false;
// const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo());
// const info2 = getDefaultControllerInfo();
// info2.username = 'cloudsa';
// const controllerModel2 = new ControllerModel(treeDataProvider, info2);
// await treeDataProvider.addOrUpdateController(controllerModel, '');
// await treeDataProvider.addOrUpdateController(controllerModel2, '');
// const children = <ControllerTreeNode[]>(await treeDataProvider.getChildren());
// await treeDataProvider.removeController(children[0]);
// should((await treeDataProvider.getChildren()).length).equal(1, 'Node should have been removed');
// await treeDataProvider.removeController(children[0]);
// should((await treeDataProvider.getChildren()).length).equal(1, 'Removing same node again should do nothing');
// await treeDataProvider.removeController(children[1]);
// should((await treeDataProvider.getChildren()).length).equal(0, 'Removing other node should work');
// await treeDataProvider.removeController(children[1]);
// should((await treeDataProvider.getChildren()).length).equal(0, 'Removing other node again should do nothing');
// });
// });
describe('openResourceDashboard', function (): void {
it('Opening dashboard for nonexistent controller node throws', async function (): Promise<void> {
const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo());
const openDashboardPromise = treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, '');
await should(openDashboardPromise).be.rejected();
});
// describe('openResourceDashboard', function (): void {
// it('Opening dashboard for nonexistent controller node throws', async function (): Promise<void> {
// const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo());
// const openDashboardPromise = treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, '');
// await should(openDashboardPromise).be.rejected();
// });
it('Opening dashboard for nonexistent resource throws', async function (): Promise<void> {
const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo());
await treeDataProvider.addOrUpdateController(controllerModel, '');
const openDashboardPromise = treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, '');
await should(openDashboardPromise).be.rejected();
});
// it('Opening dashboard for nonexistent resource throws', async function (): Promise<void> {
// const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo());
// await treeDataProvider.addOrUpdateController(controllerModel, '');
// const openDashboardPromise = treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, '');
// await should(openDashboardPromise).be.rejected();
// });
it('Opening dashboard for existing resource node succeeds', async function (): Promise<void> {
const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo());
const miaaModel = new MiaaModel(controllerModel, { name: 'miaa-1', resourceType: ResourceType.sqlManagedInstances }, undefined!, treeDataProvider);
await treeDataProvider.addOrUpdateController(controllerModel, '');
const controllerNode = treeDataProvider.getControllerNode(controllerModel)!;
const resourceNode = new MiaaTreeNode(miaaModel, controllerModel);
sinon.stub(controllerNode, 'getResourceNode').returns(resourceNode);
const showDashboardStub = sinon.stub(resourceNode, 'openDashboard');
await treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, '');
should(showDashboardStub.calledOnce).be.true('showDashboard should have been called exactly once');
});
});
});
// it('Opening dashboard for existing resource node succeeds', async function (): Promise<void> {
// const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo());
// const miaaModel = new MiaaModel(controllerModel, { name: 'miaa-1', resourceType: ResourceType.sqlManagedInstances }, undefined!, treeDataProvider);
// await treeDataProvider.addOrUpdateController(controllerModel, '');
// const controllerNode = treeDataProvider.getControllerNode(controllerModel)!;
// const resourceNode = new MiaaTreeNode(miaaModel, controllerModel);
// sinon.stub(controllerNode, 'getResourceNode').returns(resourceNode);
// const showDashboardStub = sinon.stub(resourceNode, 'openDashboard');
// await treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, '');
// should(showDashboardStub.calledOnce).be.true('showDashboard should have been called exactly once');
// });
// });
// });

View File

@@ -36,12 +36,9 @@ declare module 'arc' {
export type ControllerInfo = {
id: string,
kubeConfigFilePath: string,
kubeClusterContext: string
endpoint: string | undefined,
kubeClusterContext: string,
namespace: string,
name: string,
username: string,
rememberPassword: boolean,
resources: ResourceInfo[]
};
@@ -51,7 +48,5 @@ declare module 'arc' {
}
export interface IExtension {
getRegisteredDataControllers(): Promise<DataController[]>;
getControllerPassword(controllerInfo: ControllerInfo): Promise<string>;
reacquireControllerPassword(controllerInfo: ControllerInfo, password: string, retryCount?: number): Promise<string>;
}
}

View File

@@ -7,5 +7,5 @@
/// <reference path='../../../../src/sql/azdata.proposed.d.ts'/>
/// <reference path='../../../azurecore/src/azurecore.d.ts'/>
/// <reference path='../../../../src/vs/vscode.d.ts'/>
/// <reference path='../../../azdata/src/typings/azdata-ext.d.ts'/>
/// <reference path='../../../azcli/src/typings/az-ext.d.ts'/>
/// <reference path='../../../resource-deployment/src/typings/resource-deployment.d.ts'/>

View File

@@ -18,7 +18,7 @@ export class ControllerDashboard extends Dashboard {
public override async showDashboard(): Promise<void> {
await super.showDashboard();
// Kick off the model refresh but don't wait on it since that's all handled with callbacks anyways
this._controllerModel.refresh(false).catch(err => console.log(`Error refreshing Controller dashboard ${err}`));
this._controllerModel.refresh(false, this._controllerModel.info.namespace).catch(err => console.log(`Error refreshing Controller dashboard ${err}`));
}
protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> {

View File

@@ -58,7 +58,7 @@ export class ControllerDashboardOverviewPage extends DashboardPage {
}
protected async refresh(): Promise<void> {
await this._controllerModel.refresh();
await this._controllerModel.refresh(false, this._controllerModel.info.namespace);
}
public get container(): azdata.Component {

View File

@@ -5,7 +5,7 @@
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as azdataExt from 'azdata-ext';
import * as azExt from 'az-ext';
import * as loc from '../../../localizedConstants';
import { IconPathHelper, cssStyles } from '../../../constants';
import { DashboardPage } from '../../components/dashboardPage';
@@ -30,11 +30,11 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
memoryRequest?: string
} = {};
private readonly _azdataApi: azdataExt.IExtension;
private readonly _azApi: azExt.IExtension;
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _miaaModel: MiaaModel) {
super(modelView, dashboard);
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
this._azApi = vscode.extensions.getExtension(azExt.extension.name)?.exports;
this.initializeConfigurationBoxes();
@@ -130,8 +130,8 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
},
async (_progress, _token): Promise<void> => {
try {
await this._azdataApi.azdata.arc.sql.mi.edit(
this._miaaModel.info.name, this.saveArgs, this._miaaModel.controllerModel.azdataAdditionalEnvVars, this._miaaModel.controllerModel.controllerContext);
await this._azApi.az.sql.miarc.edit(
this._miaaModel.info.name, this.saveArgs, this._miaaModel.controllerModel.info.namespace, this._miaaModel.controllerModel.azAdditionalEnvVars);
} catch (err) {
this.saveButton!.enabled = true;
throw err;

View File

@@ -21,7 +21,7 @@ export class MiaaDashboard extends Dashboard {
public override async showDashboard(): Promise<void> {
await super.showDashboard();
// Kick off the model refreshes but don't wait on it since that's all handled with callbacks anyways
this._controllerModel.refresh().catch(err => console.log(`Error refreshing controller model for MIAA dashboard ${err}`));
this._controllerModel.refresh(false, this._controllerModel.info.namespace).catch(err => console.log(`Error refreshing controller model for MIAA dashboard ${err}`));
this._miaaModel.refresh().catch(err => console.log(`Error refreshing MIAA model for MIAA dashboard ${err}`));
}

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as azdataExt from 'azdata-ext';
import * as azExt from 'az-ext';
import * as azurecore from 'azurecore';
import * as vscode from 'vscode';
import { getDatabaseStateDisplayText, promptForInstanceDeletion } from '../../../common/utils';
@@ -34,7 +34,7 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
private _connectToServerButton!: azdata.ButtonComponent;
private _databasesTableLoading!: azdata.LoadingComponent;
private readonly _azdataApi: azdataExt.IExtension;
private readonly _azApi: azExt.IExtension;
private readonly _azurecoreApi: azurecore.IExtension;
private _instanceProperties = {
@@ -50,7 +50,7 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) {
super(modelView, dashboard);
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
this._azApi = vscode.extensions.getExtension(azExt.extension.name)?.exports;
this._azurecoreApi = vscode.extensions.getExtension(azurecore.extension.name)?.exports;
this._instanceProperties.miaaAdmin = this._miaaModel.username || this._instanceProperties.miaaAdmin;
@@ -75,7 +75,7 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
}
protected async refresh(): Promise<void> {
await Promise.all([this._controllerModel.refresh(), this._miaaModel.refresh()]);
await Promise.all([this._controllerModel.refresh(false, this._controllerModel.info.namespace), this._miaaModel.refresh()]);
}
public get container(): azdata.Component {
@@ -244,7 +244,7 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
cancellable: false
},
async (_progress, _token) => {
return await this._azdataApi.azdata.arc.sql.mi.delete(this._miaaModel.info.name, this._controllerModel.azdataAdditionalEnvVars, this._controllerModel.controllerContext);
return await this._azApi.az.sql.miarc.delete(this._miaaModel.info.name, this._controllerModel.info.namespace, this._controllerModel.azAdditionalEnvVars);
}
);
await this._controllerModel.refreshTreeNode();

View File

@@ -5,7 +5,7 @@
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as azdataExt from 'azdata-ext';
import * as azExt from 'az-ext';
import * as loc from '../../../localizedConstants';
import { IconPathHelper, cssStyles } from '../../../constants';
import { DashboardPage } from '../../components/dashboardPage';
@@ -45,11 +45,11 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
private discardButton!: azdata.ButtonComponent;
private saveButton!: azdata.ButtonComponent;
private readonly _azdataApi: azdataExt.IExtension;
private readonly _azApi: azExt.IExtension;
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _postgresModel: PostgresModel) {
super(modelView, dashboard);
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
this._azApi = vscode.extensions.getExtension(azExt.extension.name)?.exports;
this.initializeConfigurationBoxes();
@@ -165,7 +165,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
},
async (_progress, _token): Promise<void> => {
try {
await this._azdataApi.azdata.arc.postgres.server.edit(
await this._azApi.az.postgres.arcserver.edit(
this._postgresModel.info.name,
{
workers: this.saveArgs.workers,
@@ -174,7 +174,8 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
memoryRequest: this.schedulingParamsToEdit(this.saveArgs.memoryRequest!),
memoryLimit: this.schedulingParamsToEdit(this.saveArgs.memoryLimit!)
},
this._postgresModel.controllerModel.azdataAdditionalEnvVars);
this._postgresModel.controllerModel.info.namespace,
this._postgresModel.controllerModel.azAdditionalEnvVars);
} catch (err) {
// If an error occurs while editing the instance then re-enable the save button since
// the edit wasn't successfully applied

View File

@@ -36,27 +36,27 @@ export class PostgresCoordinatorNodeParametersPage extends PostgresParametersPag
}
protected async saveParameterEdits(engineSettings: string): Promise<void> {
await this._azdataApi.azdata.arc.postgres.server.edit(
await this._azApi.az.postgres.arcserver.edit(
this._postgresModel.info.name,
{ coordinatorEngineSettings: engineSettings },
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
this._postgresModel.controllerModel.controllerContext);
this._postgresModel.controllerModel.info.namespace,
this._postgresModel.controllerModel.azAdditionalEnvVars);
}
protected async resetAllParameters(): Promise<void> {
await this._azdataApi.azdata.arc.postgres.server.edit(
await this._azApi.az.postgres.arcserver.edit(
this._postgresModel.info.name,
{ coordinatorEngineSettings: `''`, replaceEngineSettings: true },
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
this._postgresModel.controllerModel.controllerContext);
this._postgresModel.controllerModel.info.namespace,
this._postgresModel.controllerModel.azAdditionalEnvVars);
}
protected async resetParameter(parameterName: string): Promise<void> {
await this._azdataApi.azdata.arc.postgres.server.edit(
await this._azApi.az.postgres.arcserver.edit(
this._postgresModel.info.name,
{ coordinatorEngineSettings: parameterName + '=' },
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
this._postgresModel.controllerModel.controllerContext);
this._postgresModel.controllerModel.info.namespace,
this._postgresModel.controllerModel.azAdditionalEnvVars);
}
}

View File

@@ -29,7 +29,7 @@ export class PostgresDashboard extends Dashboard {
await super.showDashboard();
// Kick off the model refresh but don't wait on it since that's all handled with callbacks anyways
this._controllerModel.refresh().catch(err => console.log(`Error refreshing controller model for Postgres dashboard ${err}`));
this._controllerModel.refresh(false, this._controllerModel.info.namespace).catch(err => console.log(`Error refreshing controller model for Postgres dashboard ${err}`));
this._postgresModel.refresh().catch(err => console.log(`Error refreshing Postgres model for Postgres dashboard ${err}`));
}

View File

@@ -5,7 +5,7 @@
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as azdataExt from 'azdata-ext';
import * as azExt from 'az-ext';
import * as loc from '../../../localizedConstants';
import { IconPathHelper, cssStyles } from '../../../constants';
import { DashboardPage } from '../../components/dashboardPage';
@@ -22,11 +22,11 @@ export class PostgresExtensionsPage extends DashboardPage {
private dropExtensionsButton!: azdata.ButtonComponent;
private extensionsLink!: azdata.HyperlinkComponent;
private readonly _azdataApi: azdataExt.IExtension;
private readonly _azApi: azExt.IExtension;
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _postgresModel: PostgresModel) {
super(modelView, dashboard);
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
this._azApi = vscode.extensions.getExtension(azExt.extension.name)?.exports;
this.disposables.push(
this._postgresModel.onConfigUpdated(() => this.eventuallyRunOnInitialized(() => this.handleConfigUpdated())));
@@ -138,12 +138,13 @@ export class PostgresExtensionsPage extends DashboardPage {
},
async (_progress, _token): Promise<void> => {
await this._azdataApi.azdata.arc.postgres.server.edit(
await this._azApi.az.postgres.arcserver.edit(
this._postgresModel.info.name,
{
extensions: extensionList
},
this._postgresModel.controllerModel.azdataAdditionalEnvVars);
this._postgresModel.controllerModel.info.namespace,
this._postgresModel.controllerModel.azAdditionalEnvVars);
try {
await this._postgresModel.refresh();
@@ -235,6 +236,10 @@ export class PostgresExtensionsPage extends DashboardPage {
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
if (name === 'citus') {
checkBox.enabled = false;
}
this.disposables.push(
checkBox.onChanged(() => {
if (checkBox.checked) {
@@ -256,7 +261,7 @@ export class PostgresExtensionsPage extends DashboardPage {
*/
public async dropExtension(): Promise<void> {
this.droppedExtensions.forEach(d => {
let index = this.droppedExtensions.indexOf(d, 0);
let index = this.extensionNames.indexOf(d, 0);
this.extensionNames.splice(index, 1);
});
@@ -267,12 +272,13 @@ export class PostgresExtensionsPage extends DashboardPage {
cancellable: false
},
async (_progress, _token): Promise<void> => {
await this._azdataApi.azdata.arc.postgres.server.edit(
await this._azApi.az.postgres.arcserver.edit(
this._postgresModel.info.name,
{
extensions: this.extensionNames.join()
},
this._postgresModel.controllerModel.azdataAdditionalEnvVars
this._postgresModel.controllerModel.info.namespace,
this._postgresModel.controllerModel.azAdditionalEnvVars
);
}
);

View File

@@ -5,7 +5,7 @@
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as azdataExt from 'azdata-ext';
import * as azExt from 'az-ext';
import * as loc from '../../../localizedConstants';
import { IconPathHelper, cssStyles, iconSize } from '../../../constants';
import { DashboardPage } from '../../components/dashboardPage';
@@ -35,11 +35,11 @@ export class PostgresOverviewPage extends DashboardPage {
private podStatusTable!: azdata.DeclarativeTableComponent;
private podStatusData: PodStatusModel[] = [];
private readonly _azdataApi: azdataExt.IExtension;
private readonly _azApi: azExt.IExtension;
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
super(modelView, dashboard);
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
this._azApi = vscode.extensions.getExtension(azExt.extension.name)?.exports;
this.disposables.push(
this._controllerModel.onRegistrationsUpdated(() => this.eventuallyRunOnInitialized(() => this.handleRegistrationsUpdated())),
@@ -223,13 +223,14 @@ export class PostgresOverviewPage extends DashboardPage {
try {
const password = await promptAndConfirmPassword(input => !input ? loc.enterANonEmptyPassword : '');
if (password) {
await this._azdataApi.azdata.arc.postgres.server.edit(
await this._azApi.az.postgres.arcserver.edit(
this._postgresModel.info.name,
{
adminPassword: true,
noWait: true
},
Object.assign({ 'AZDATA_PASSWORD': password }, this._controllerModel.azdataAdditionalEnvVars));
this._postgresModel.controllerModel.info.namespace,
Object.assign({ 'AZDATA_PASSWORD': password }, this._controllerModel.azAdditionalEnvVars));
vscode.window.showInformationMessage(loc.passwordReset);
}
} catch (error) {
@@ -257,7 +258,7 @@ export class PostgresOverviewPage extends DashboardPage {
cancellable: false
},
async (_progress, _token) => {
return await this._azdataApi.azdata.arc.postgres.server.delete(this._postgresModel.info.name, this._controllerModel.azdataAdditionalEnvVars, this._controllerModel.controllerContext);
return await this._azApi.az.postgres.arcserver.delete(this._postgresModel.info.name, this._postgresModel.controllerModel.info.namespace, this._controllerModel.azAdditionalEnvVars);
}
);
await this._controllerModel.refreshTreeNode();
@@ -294,7 +295,7 @@ export class PostgresOverviewPage extends DashboardPage {
await Promise.all([
this._postgresModel.refresh(),
this._controllerModel.refresh()
this._controllerModel.refresh(false, this._controllerModel.info.namespace)
]);
} catch (error) {
vscode.window.showErrorMessage(loc.refreshFailed(error));
@@ -351,7 +352,7 @@ export class PostgresOverviewPage extends DashboardPage {
let podModels: PodStatusModel[] = [];
const podStatus = this._postgresModel.config?.status.podsStatus;
podStatus?.forEach(p => {
podStatus?.forEach((p: { conditions: any[]; name: any; role: string; }) => {
// If a condition of the pod has a status of False, pod is not Ready
const status = p.conditions.find(c => c.status === 'False') ? loc.notReady : loc.ready;

View File

@@ -5,7 +5,7 @@
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as azdataExt from 'azdata-ext';
import * as azExt from 'az-ext';
import * as loc from '../../../localizedConstants';
import { UserCancelledError } from '../../../common/api';
import { IconPathHelper, cssStyles } from '../../../constants';
@@ -37,12 +37,12 @@ export abstract class PostgresParametersPage extends DashboardPage {
private changedComponentValues: Set<string> = new Set();
private parameterUpdates: Map<string, string> = new Map();
protected readonly _azdataApi: azdataExt.IExtension;
protected readonly _azApi: azExt.IExtension;
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, protected _postgresModel: PostgresModel) {
super(modelView, dashboard);
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
this._azApi = vscode.extensions.getExtension(azExt.extension.name)?.exports;
this.initializeSearchBox();

View File

@@ -77,7 +77,7 @@ export class PostgresPropertiesPage extends DashboardPage {
this.loading!.loading = true;
await Promise.all([
this._postgresModel.refresh(),
this._controllerModel.refresh()
this._controllerModel.refresh(false, this._controllerModel.info.namespace)
]);
} catch (error) {
vscode.window.showErrorMessage(loc.refreshFailed(error));

View File

@@ -37,26 +37,26 @@ export class PostgresWorkerNodeParametersPage extends PostgresParametersPage {
}
protected async saveParameterEdits(engineSettings: string): Promise<void> {
await this._azdataApi.azdata.arc.postgres.server.edit(
await this._azApi.az.postgres.arcserver.edit(
this._postgresModel.info.name,
{ workerEngineSettings: engineSettings },
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
this._postgresModel.controllerModel.controllerContext);
this._postgresModel.controllerModel.info.namespace,
this._postgresModel.controllerModel.azAdditionalEnvVars);
}
protected async resetAllParameters(): Promise<void> {
await this._azdataApi.azdata.arc.postgres.server.edit(
await this._azApi.az.postgres.arcserver.edit(
this._postgresModel.info.name,
{ workerEngineSettings: `''`, replaceEngineSettings: true },
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
this._postgresModel.controllerModel.controllerContext);
this._postgresModel.controllerModel.info.namespace,
this._postgresModel.controllerModel.azAdditionalEnvVars);
}
protected async resetParameter(parameterName: string): Promise<void> {
await this._azdataApi.azdata.arc.postgres.server.edit(
await this._azApi.az.postgres.arcserver.edit(
this._postgresModel.info.name,
{ workerEngineSettings: parameterName + '=' },
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
this._postgresModel.controllerModel.controllerContext);
this._postgresModel.controllerModel.info.namespace,
this._postgresModel.controllerModel.azAdditionalEnvVars);
}
}

View File

@@ -10,6 +10,8 @@ import { cssStyles } from '../../constants';
import { InitializingComponent } from '../components/initializingComponent';
import { PostgresModel } from '../../models/postgresModel';
export const validExtensions = ['citus', 'pgaudit', 'pgautofailover', 'pg_cron', 'pg_partman', 'plv8', 'postgis', 'postgis_raster', 'postgis_sfcgal', 'postgis_tiger_geocoder', 'tdigest'];
export class AddPGExtensionsDialog extends InitializingComponent {
protected modelBuilder!: azdata.ModelBuilder;
@@ -28,7 +30,7 @@ export class AddPGExtensionsDialog extends InitializingComponent {
this.modelBuilder = view.modelBuilder;
const info = this.modelBuilder.text().withProps({
value: loc.extensionsFunction,
value: loc.extensionsAddFunction(validExtensions.join(', ')),
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
@@ -45,7 +47,15 @@ export class AddPGExtensionsDialog extends InitializingComponent {
.withProps({
value: '',
ariaLabel: loc.extensionsAddList,
enabled: true
enabled: true,
validationErrorMessage: loc.extensionsAddErrorrMessage(validExtensions.join(','))
}).withValidation((component) => {
if (!component.value) {
return true;
}
let newExtensions = component.value.split(',');
return newExtensions.every(e => validExtensions.includes(e));
}).component();
let formModel = this.modelBuilder.formContainer()
@@ -56,7 +66,8 @@ export class AddPGExtensionsDialog extends InitializingComponent {
},
{
component: this.extensionsListInputBox,
title: loc.extensionsAddList
title: loc.extensionsAddList,
required: true
}
],
title: ''

View File

@@ -5,7 +5,6 @@
import { ControllerInfo, ResourceInfo } from 'arc';
import * as azdata from 'azdata';
import * as azdataExt from 'azdata-ext';
import { v4 as uuid } from 'uuid';
import * as vscode from 'vscode';
import { Deferred } from '../../common/promise';
@@ -13,12 +12,11 @@ import * as loc from '../../localizedConstants';
import { ControllerModel } from '../../models/controllerModel';
import { InitializingComponent } from '../components/initializingComponent';
import { AzureArcTreeDataProvider } from '../tree/azureArcTreeDataProvider';
import { getErrorMessage } from '../../common/utils';
import { RadioOptionsGroup } from '../components/radioOptionsGroup';
import { getCurrentClusterContext, getDefaultKubeConfigPath, getKubeConfigClusterContexts, KubeClusterContext } from '../../common/kubeUtils';
import { FilePicker } from '../components/filePicker';
export type ConnectToControllerDialogModel = { controllerModel: ControllerModel, password: string };
export type ConnectToControllerDialogModel = { controllerModel: ControllerModel };
abstract class ControllerDialogBase extends InitializingComponent {
protected _toDispose: vscode.Disposable[] = [];
@@ -29,9 +27,6 @@ abstract class ControllerDialogBase extends InitializingComponent {
protected kubeConfigInputBox!: FilePicker;
protected clusterContextRadioGroup!: RadioOptionsGroup;
protected nameInputBox!: azdata.InputBoxComponent;
protected usernameInputBox!: azdata.InputBoxComponent;
protected passwordInputBox!: azdata.InputBoxComponent;
protected urlInputBox!: azdata.InputBoxComponent;
private _kubeClusters: KubeClusterContext[] = [];
@@ -46,13 +41,6 @@ abstract class ControllerDialogBase extends InitializingComponent {
component: this.namespaceInputBox,
title: loc.namespace,
required: true
},
{
component: this.urlInputBox,
title: loc.controllerUrl,
layout: {
info: loc.controllerUrlDescription
}
}, {
component: this.kubeConfigInputBox.component(),
title: loc.controllerKubeConfig,
@@ -68,14 +56,6 @@ abstract class ControllerDialogBase extends InitializingComponent {
layout: {
info: loc.controllerNameDescription
}
}, {
component: this.usernameInputBox,
title: loc.controllerUsername,
required: true
}, {
component: this.passwordInputBox,
title: loc.controllerPassword,
required: true
}
];
}
@@ -83,16 +63,11 @@ abstract class ControllerDialogBase extends InitializingComponent {
protected abstract fieldToFocusOn(): azdata.Component;
protected readonlyFields(): azdata.Component[] { return []; }
protected initializeFields(controllerInfo: ControllerInfo | undefined, password: string | undefined) {
protected initializeFields(controllerInfo: ControllerInfo | undefined) {
this.namespaceInputBox = this.modelBuilder.inputBox()
.withProps({
value: controllerInfo?.namespace,
}).component();
this.urlInputBox = this.modelBuilder.inputBox()
.withProps({
value: controllerInfo?.endpoint,
placeHolder: loc.controllerUrlPlaceholder,
}).component();
this.kubeConfigInputBox = new FilePicker(
this.modelBuilder,
controllerInfo?.kubeConfigFilePath || getDefaultKubeConfigPath(),
@@ -113,15 +88,6 @@ abstract class ControllerDialogBase extends InitializingComponent {
.withProps({
value: controllerInfo?.name
}).component();
this.usernameInputBox = this.modelBuilder.inputBox()
.withProps({
value: controllerInfo?.username
}).component();
this.passwordInputBox = this.modelBuilder.inputBox()
.withProps({
inputType: 'password',
value: password
}).component();
}
protected completionPromise = new Deferred<ConnectToControllerDialogModel | undefined>();
@@ -150,13 +116,13 @@ abstract class ControllerDialogBase extends InitializingComponent {
this.namespaceInputBox.value = currentContext?.namespace;
}
public showDialog(controllerInfo?: ControllerInfo, password: string | undefined = undefined): azdata.window.Dialog {
public showDialog(controllerInfo?: ControllerInfo): azdata.window.Dialog {
this.id = controllerInfo?.id ?? uuid();
this.resources = controllerInfo?.resources ?? [];
this._toDispose.push(this.dialog.cancelButton.onClick(() => this.handleCancel()));
this.dialog.registerContent(async (view) => {
this.modelBuilder = view.modelBuilder;
this.initializeFields(controllerInfo, password);
this.initializeFields(controllerInfo);
let formModel = this.modelBuilder.formContainer()
.withFormItems([{
@@ -192,75 +158,37 @@ abstract class ControllerDialogBase extends InitializingComponent {
return this.completionPromise.promise;
}
protected getControllerInfo(url: string, rememberPassword: boolean = false): ControllerInfo {
protected getControllerInfo(): ControllerInfo {
return {
id: this.id,
endpoint: url || undefined,
namespace: this.namespaceInputBox.value!.trim(),
kubeConfigFilePath: this.kubeConfigInputBox.value!,
kubeClusterContext: this.clusterContextRadioGroup.value!,
name: this.nameInputBox.value ?? '',
username: this.usernameInputBox.value!,
rememberPassword: rememberPassword,
resources: this.resources
};
}
}
export class ConnectToControllerDialog extends ControllerDialogBase {
protected rememberPwCheckBox!: azdata.CheckBoxComponent;
protected fieldToFocusOn() {
return this.namespaceInputBox;
}
protected override getComponents() {
return [
...super.getComponents(),
{
component: this.rememberPwCheckBox,
title: ''
}];
}
protected override initializeFields(controllerInfo: ControllerInfo | undefined, password: string | undefined) {
super.initializeFields(controllerInfo, password);
this.rememberPwCheckBox = this.modelBuilder.checkBox()
.withProps({
label: loc.rememberPassword,
checked: controllerInfo?.rememberPassword
}).component();
}
constructor(treeDataProvider: AzureArcTreeDataProvider) {
super(treeDataProvider, loc.connectToController);
}
public async validate(): Promise<boolean> {
if (!this.namespaceInputBox.value || !this.usernameInputBox.value || !this.passwordInputBox.value) {
if (!this.namespaceInputBox.value) {
return false;
}
let url = this.urlInputBox.value?.trim() || '';
if (url) {
// Only support https connections
if (url.toLowerCase().startsWith('http://')) {
url = url.replace('http', 'https');
}
// Append https if they didn't type it in
if (!url.toLowerCase().startsWith('https://')) {
url = `https://${url}`;
}
// Append default port if one wasn't specified
if (!/.*:\d*$/.test(url)) {
url = `${url}:30080`;
}
}
const controllerInfo: ControllerInfo = this.getControllerInfo(url, !!this.rememberPwCheckBox.checked);
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
const controllerInfo: ControllerInfo = this.getControllerInfo();
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo);
try {
// Validate that we can connect to the controller, this also populates the controllerRegistration from the connection response.
await controllerModel.refresh(false);
await controllerModel.refresh(false, this.namespaceInputBox.value);
// default info.name to the name of the controller instance if the user did not specify their own and to a pre-canned default if for some weird reason controller endpoint returned instanceName is also not a valid value
controllerModel.info.name = controllerModel.info.name || controllerModel.controllerConfig?.metadata.name || loc.defaultControllerName;
} catch (err) {
@@ -270,74 +198,7 @@ export class ConnectToControllerDialog extends ControllerDialogBase {
};
return false;
}
this.completionPromise.resolve({ controllerModel: controllerModel, password: this.passwordInputBox.value });
this.completionPromise.resolve({ controllerModel: controllerModel });
return true;
}
}
export class PasswordToControllerDialog extends ControllerDialogBase {
constructor(treeDataProvider: AzureArcTreeDataProvider) {
super(treeDataProvider, loc.passwordToController);
}
protected fieldToFocusOn() {
return this.passwordInputBox;
}
protected override readonlyFields(): azdata.Component[] {
return [
this.urlInputBox,
...this.kubeConfigInputBox.items,
...this.clusterContextRadioGroup.items,
this.nameInputBox,
this.usernameInputBox
];
}
public async validate(): Promise<boolean> {
if (!this.passwordInputBox.value) {
return false;
}
const controllerInfo: ControllerInfo = this.getControllerInfo(this.urlInputBox.value!, false);
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
const azdataApi = <azdataExt.IExtension>vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
try {
await azdataApi.azdata.login(
{
endpoint: controllerInfo.endpoint,
namespace: controllerInfo.namespace
},
controllerInfo.username,
this.passwordInputBox.value,
{
'KUBECONFIG': this.kubeConfigInputBox.value!,
'KUBECTL_CONTEXT': this.clusterContextRadioGroup.value!
}
);
} catch (e) {
if (getErrorMessage(e).match(/Wrong username or password/i)) {
this.dialog.message = {
text: loc.loginFailed,
level: azdata.window.MessageLevel.Error
};
return false;
} else {
this.dialog.message = {
text: loc.errorVerifyingPassword(e),
level: azdata.window.MessageLevel.Error
};
return false;
}
}
this.completionPromise.resolve({ controllerModel: controllerModel, password: this.passwordInputBox.value });
return true;
}
public override showDialog(controllerInfo?: ControllerInfo): azdata.window.Dialog {
const dialog = super.showDialog(controllerInfo);
dialog.okButton.label = loc.ok;
return dialog;
}
}

View File

@@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/
import { ControllerInfo } from 'arc';
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { ControllerModel } from '../../models/controllerModel';
import { ControllerTreeNode } from './controllerTreeNode';
@@ -18,7 +17,6 @@ const mementoToken = 'arcDataControllers.v2';
*/
export class AzureArcTreeDataProvider implements vscode.TreeDataProvider<TreeNode> {
private _credentialsProvider = azdata.credentials.getProvider('arcControllerPasswords');
private _onDidChangeTreeData: vscode.EventEmitter<TreeNode | undefined> = new vscode.EventEmitter<TreeNode | undefined>();
readonly onDidChangeTreeData: vscode.Event<TreeNode | undefined> = this._onDidChangeTreeData.event;
@@ -51,14 +49,13 @@ export class AzureArcTreeDataProvider implements vscode.TreeDataProvider<TreeNod
return element;
}
public async addOrUpdateController(model: ControllerModel, password: string, refreshTree = true): Promise<void> {
public async addOrUpdateController(model: ControllerModel, refreshTree = true): Promise<void> {
const controllerNode = this.getControllerNode(model);
if (controllerNode) {
controllerNode.model.info = model.info;
} else {
this._controllerNodes.push(new ControllerTreeNode(model, this._context, this));
}
await this.updatePassword(model, password);
if (refreshTree) {
this._onDidChangeTreeData.fire(undefined);
}
@@ -71,22 +68,10 @@ export class AzureArcTreeDataProvider implements vscode.TreeDataProvider<TreeNod
public async removeController(controllerNode: ControllerTreeNode): Promise<void> {
this._controllerNodes = this._controllerNodes.filter(node => node !== controllerNode);
await this.deletePassword(controllerNode.model.info);
this._onDidChangeTreeData.fire(undefined);
await this.saveControllers();
}
public async getPassword(info: ControllerInfo): Promise<string> {
const provider = await this._credentialsProvider;
const credential = await provider.readCredential(info.id);
return credential.password;
}
private async deletePassword(info: ControllerInfo): Promise<void> {
const provider = await this._credentialsProvider;
await provider.deleteCredential(info.id);
}
/**
* Refreshes the specified node, or the entire tree if node is undefined
* @param node The node to refresh, or undefined for the whole tree
@@ -95,15 +80,6 @@ export class AzureArcTreeDataProvider implements vscode.TreeDataProvider<TreeNod
this._onDidChangeTreeData.fire(node);
}
private async updatePassword(model: ControllerModel, password: string): Promise<void> {
const provider = await this._credentialsProvider;
if (model.info.rememberPassword) {
await provider.saveCredential(model.info.id, password);
} else {
await provider.deleteCredential(model.info.id);
}
}
private async loadSavedControllers(): Promise<void> {
try {
const controllerMementos: ControllerInfo[] = this._context.globalState.get(mementoToken) || [];

View File

@@ -39,12 +39,12 @@ export class ControllerTreeNode extends TreeNode {
public override async getChildren(): Promise<TreeNode[]> {
try {
await this.model.refresh(false);
await this.model.refresh(false, this.model.info.namespace);
this.updateChildren(this.model.registrations);
} catch (err) {
vscode.window.showErrorMessage(loc.errorConnectingToController(err));
try {
await this.model.refresh(false);
await this.model.refresh(false, this.model.info.namespace);
this.updateChildren(this.model.registrations);
} catch (err) {
if (!(err instanceof UserCancelledError)) {