[Port] Sync up arc and azdata extensions with main (#12810)

* Sync up arc and azdata extensions with main

* capture 'this' to use retrieveVariable as callback (#12828)

* capture 'this' to use retrieveVariable as callback

* remove change not needed for #12082

Co-authored-by: Arvind Ranasaria <ranasaria@outlook.com>
This commit is contained in:
Charles Gagnon
2020-10-08 16:03:27 -07:00
committed by GitHub
parent 6adeffbc8e
commit 7429407029
57 changed files with 1246 additions and 1509 deletions

View File

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

View File

@@ -8,5 +8,5 @@
not_numbered: true
expand_sections: true
sections:
- title: TSG100 - The Azure Arc Postgres troubleshooter
- title: TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter
url: postgres/tsg100-troubleshoot-postgres

View File

@@ -3,5 +3,5 @@
- This chapter contains notebooks for troubleshooting Postgres on Azure Arc
## Notebooks in this Chapter
- [TSG100 - The Azure Arc Postgres troubleshooter](tsg100-troubleshoot-postgres.ipynb)
- [TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter](tsg100-troubleshoot-postgres.ipynb)

View File

@@ -3,5 +3,5 @@
not_numbered: true
expand_sections: true
sections:
- title: TSG100 - The Azure Arc Postgres troubleshooter
- title: TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter
url: postgres/tsg100-troubleshoot-postgres

View File

@@ -4,13 +4,14 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"TSG100 - The Azure Arc Postgres troubleshooter\n",
"==============================================\n",
"TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter\n",
"===================================================================\n",
"\n",
"Description\n",
"-----------\n",
"\n",
"Follow these steps to troubleshoot an Azure Arc Postgres Server.\n",
"Follow these steps to troubleshoot an Azure Arc enabled PostgreSQL\n",
"Hyperscale Server.\n",
"\n",
"Steps\n",
"-----\n",
@@ -34,6 +35,7 @@
"# the user will be prompted to select a server.\n",
"namespace = os.environ.get('POSTGRES_SERVER_NAMESPACE')\n",
"name = os.environ.get('POSTGRES_SERVER_NAME')\n",
"version = os.environ.get('POSTGRES_SERVER_VERSION')\n",
"\n",
"tail_lines = 50"
]
@@ -143,7 +145,7 @@
" if cmd.startswith(\"kubectl \") and \"AZDATA_OPENSHIFT\" in os.environ:\n",
" cmd_actual[0] = cmd_actual[0].replace(\"kubectl\", \"oc\")\n",
"\n",
" # To aid supportabilty, determine which binary file will actually be executed on the machine\n",
" # To aid supportability, determine which binary file will actually be executed on the machine\n",
" #\n",
" which_binary = None\n",
"\n",
@@ -400,11 +402,11 @@
"import math\n",
"\n",
"# If a server was provided, get it\n",
"if namespace and name:\n",
" server = json.loads(run(f'kubectl get dbs -n {namespace} {name} -o json', return_output=True))\n",
"if namespace and name and version:\n",
" server = json.loads(run(f'kubectl get postgresql-{version} -n {namespace} {name} -o json', return_output=True))\n",
"else:\n",
" # Otherwise prompt the user to select a server\n",
" servers = json.loads(run(f'kubectl get dbs --all-namespaces -o json', return_output=True))['items']\n",
" servers = json.loads(run(f'kubectl get postgresqls --all-namespaces -o json', return_output=True))['items']\n",
" if not servers:\n",
" raise Exception('No Postgres servers found')\n",
"\n",
@@ -425,6 +427,7 @@
" server = servers[i-1]\n",
" namespace = server['metadata']['namespace']\n",
" name = server['metadata']['name']\n",
" version = server['kind'][len('postgresql-'):]\n",
" break\n",
"\n",
"display(Markdown(f'#### Got server {namespace}.{name}'))"
@@ -446,10 +449,10 @@
"uid = server['metadata']['uid']\n",
"\n",
"display(Markdown(f'#### Server summary'))\n",
"run(f'kubectl get dbs -n {namespace} {name}')\n",
"run(f'kubectl get postgresql-{version} -n {namespace} {name}')\n",
"\n",
"display(Markdown(f'#### Resource summary'))\n",
"run(f'kubectl get pods,pvc,svc,ep -n {namespace} -l dusky.microsoft.com/serviceId={uid}')"
"run(f'kubectl get sts,pods,pvc,svc,ep -n {namespace} -l postgresqls.arcdata.microsoft.com/cluster-id={uid}')"
]
},
{
@@ -466,7 +469,7 @@
"outputs": [],
"source": [
"display(Markdown(f'#### Troubleshooting server {namespace}.{name}'))\n",
"run(f'kubectl describe dbs -n {namespace} {name}')"
"run(f'kubectl describe postgresql-{version} -n {namespace} {name}')"
]
},
{
@@ -482,7 +485,7 @@
"metadata": {},
"outputs": [],
"source": [
"pods = json.loads(run(f'kubectl get pods -n {namespace} -l dusky.microsoft.com/serviceId={uid} -o json', return_output=True))['items']\n",
"pods = json.loads(run(f'kubectl get pods -n {namespace} -l postgresqls.arcdata.microsoft.com/cluster-id={uid} -o json', return_output=True))['items']\n",
"\n",
"# Summarize and describe each pod\n",
"for pod in pods:\n",
@@ -529,8 +532,7 @@
" con_restarts = con_status.get('restartCount', 0)\n",
"\n",
" display(Markdown(f'#### Troubleshooting container {namespace}.{pod_name}/{con_name} ({i+1}/{len(cons)})\\n'\n",
" f'#### {\"S\" if con_started else \"Not s\"}tarted and '\n",
" f'{\"\" if con_ready else \"not \"}ready with {con_restarts} restarts'))\n",
" f'#### {\"R\" if con_ready else \"Not r\"}eady with {con_restarts} restarts'))\n",
"\n",
" run(f'kubectl logs -n {namespace} {pod_name} {con_name} --tail {tail_lines}')\n",
"\n",
@@ -554,7 +556,7 @@
"outputs": [],
"source": [
"display(Markdown(f'#### Troubleshooting PersistentVolumeClaims'))\n",
"run(f'kubectl describe pvc -n {namespace} -l dusky.microsoft.com/serviceId={uid}')"
"run(f'kubectl describe pvc -n {namespace} -l postgresqls.arcdata.microsoft.com/cluster-id={uid}')"
]
},
{

View File

@@ -47,7 +47,7 @@
"|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",
"|azdata | Command-line tool for installing and managing resources in an Azure Arc cluster |[Installation](https://github.com/microsoft/Azure-data-services-on-Azure-Arc/blob/master/scenarios/001-install-client-tools.md) |"
"|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) |"
],
"metadata": {
"azdata_cell_guid": "714582b9-10ee-409e-ab12-15a4825c9471"
@@ -90,7 +90,7 @@
"cell_type": "markdown",
"source": [
"### **Set variables**\n",
"Generated by Azure Data Studio using the values collected in the Azure Arc Data controller create wizard"
"Generated by Azure Data Studio using the values collected in the 'Create Azure Arc data controller' wizard."
],
"metadata": {
"azdata_cell_guid": "4b266b2d-bd1b-4565-92c9-3fc146cdce6d"
@@ -129,13 +129,11 @@
{
"cell_type": "code",
"source": [
"if \"AZDATA_NB_VAR_ARC_DOCKER_PASSWORD\" in os.environ:\n",
" arc_docker_password = os.environ[\"AZDATA_NB_VAR_ARC_DOCKER_PASSWORD\"]\n",
"if \"AZDATA_NB_VAR_ARC_ADMIN_PASSWORD\" in os.environ:\n",
" arc_admin_password = os.environ[\"AZDATA_NB_VAR_ARC_ADMIN_PASSWORD\"]\n",
"else:\n",
" if arc_admin_password == \"\":\n",
" arc_admin_password = getpass.getpass(prompt = 'Azure Arc Data controller password')\n",
" arc_admin_password = getpass.getpass(prompt = 'Azure Arc Data Controller password')\n",
" if arc_admin_password == \"\":\n",
" sys.exit(f'Password is required.')\n",
" confirm_password = getpass.getpass(prompt = 'Confirm password')\n",
@@ -175,7 +173,7 @@
{
"cell_type": "markdown",
"source": [
"### **Create Azure Arc Data controller**"
"### **Create Azure Arc Data Controller**"
],
"metadata": {
"azdata_cell_guid": "efe78cd3-ed73-4c9b-b586-fdd6c07dd37f"
@@ -184,16 +182,14 @@
{
"cell_type": "code",
"source": [
"print (f'Creating Azure Arc controller: {arc_data_controller_name} using configuration {arc_cluster_context}')\n",
"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",
"os.environ[\"DOCKER_USERNAME\"] = arc_docker_username\n",
"os.environ[\"DOCKER_PASSWORD\"] = arc_docker_password\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 {arc_data_controller_connectivity_mode} -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'Azure Arc Data controller cluster: {arc_data_controller_name} created.') "
"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'Azure Arc Data Controller: {arc_data_controller_name} created.') "
],
"metadata": {
"azdata_cell_guid": "373947a1-90b9-49ee-86f4-17a4c7d4ca76",
@@ -205,7 +201,7 @@
{
"cell_type": "markdown",
"source": [
"### **Setting context to created Azure Arc Data controller**"
"### **Setting context to created Azure Arc Data Controller**"
],
"metadata": {
"azdata_cell_guid": "a3ddc701-811d-4058-b3fb-b7295fcf50ae"
@@ -214,7 +210,7 @@
{
"cell_type": "code",
"source": [
"# Setting context to data controller.\n",
"# Setting context to Data Controller.\n",
"#\n",
"run_command(f'kubectl config set-context --current --namespace {arc_data_controller_namespace}')"
],
@@ -227,7 +223,7 @@
{
"cell_type": "markdown",
"source": [
"### **Login to the data controller.**\n"
"### **Login to the Data Controller.**\n"
],
"metadata": {
"azdata_cell_guid": "9376b2ab-0edf-478f-9e3c-5ff46ae3501a"
@@ -236,9 +232,9 @@
{
"cell_type": "code",
"source": [
"# Login to the data controller.\n",
"# Login to the Data Controller.\n",
"#\n",
"run_command(f'azdata login -n {arc_data_controller_namespace}')"
"run_command(f'azdata login --namespace {arc_data_controller_namespace}')"
],
"metadata": {
"azdata_cell_guid": "9aed0c5a-2c8a-4ad7-becb-60281923a196"

View File

@@ -25,12 +25,12 @@
"source": [
"![Microsoft](https://raw.githubusercontent.com/microsoft/azuredatastudio/main/extensions/arc/images/microsoft-small-logo.png)\n",
" \n",
"## Deploy a PostgreSQL server group on an existing Azure Arc data cluster\n",
"## Create a PostgreSQL Hyperscale - Azure Arc on an existing Azure Arc Data Controller\n",
" \n",
"This notebook walks through the process of deploying a PostgreSQL server group on an existing Azure Arc data cluster.\n",
"This notebook walks through the process of creating a PostgreSQL Hyperscale - Azure Arc on an existing Azure Arc Data Controller.\n",
" \n",
"* Follow the instructions in the **Prerequisites** cell to install the tools if not already installed.\n",
"* Make sure you have the target Azure Arc data cluster already created.\n",
"* Make sure you have the target Azure Arc Data Controller already created.\n",
"\n",
"<span style=\"color:red\"><font size=\"3\">Please press the \"Run All\" button to run the notebook</font></span>"
],
@@ -41,7 +41,21 @@
{
"cell_type": "markdown",
"source": [
"### **Check prerequisites**"
"### **Prerequisites** \n",
"Ensure the following tools are installed and added to PATH before proceeding.\n",
" \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) |"
],
"metadata": {
"azdata_cell_guid": "20fe3985-a01e-461c-bce0-235f7606cc3c"
}
},
{
"cell_type": "markdown",
"source": [
"### **Setup and Check Prerequisites**"
],
"metadata": {
"azdata_cell_guid": "68531b91-ddce-47d7-a1d8-2ddc3d17f3e7"
@@ -75,100 +89,20 @@
{
"cell_type": "markdown",
"source": [
"#### **Ensure Postgres Server Group name and password exist**"
"### **Set variables**\n",
"\n",
"#### \n",
"\n",
"Generated by Azure Data Studio using the values collected in the 'Deploy PostgreSQL Hyperscale - Azure Arc instance' wizard"
],
"metadata": {
"azdata_cell_guid": "68ec0760-27d1-4ded-9a9f-89077c40b8bb"
}
},
{
"cell_type": "code",
"source": [
"# Required Values\n",
"env_var = \"AZDATA_NB_VAR_CONTROLLER_ENDPOINT\" in os.environ\n",
"if env_var:\n",
" controller_endpoint = os.environ[\"AZDATA_NB_VAR_CONTROLLER_ENDPOINT\"]\n",
"else:\n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_CONTROLLER_ENDPOINT was not defined. Exiting\\n')\n",
"\n",
"env_var = \"AZDATA_NB_VAR_CONTROLLER_USERNAME\" in os.environ\n",
"if env_var:\n",
" controller_username = os.environ[\"AZDATA_NB_VAR_CONTROLLER_USERNAME\"]\n",
"else:\n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_CONTROLLER_USERNAME was not defined. Exiting\\n')\n",
"\n",
"env_var = \"AZDATA_NB_VAR_CONTROLLER_PASSWORD\" in os.environ\n",
"if env_var:\n",
" controller_password = os.environ[\"AZDATA_NB_VAR_CONTROLLER_PASSWORD\"]\n",
"else:\n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_CONTROLLER_PASSWORD was not defined. Exiting\\n')\n",
"\n",
"env_var = \"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_NAME\" in os.environ\n",
"if env_var:\n",
" server_group_name = os.environ[\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_NAME\"]\n",
"else:\n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_NAME was not defined. Exiting\\n')\n",
"\n",
"env_var = \"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_PASSWORD\" in os.environ\n",
"if env_var:\n",
" postgres_password = os.environ[\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_PASSWORD\"]\n",
"else:\n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_PASSWORD was not defined. Exiting\\n') \n",
"\n",
"env_var = \"AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_DATA\" in os.environ\n",
"if env_var:\n",
" postgres_storage_class_data = os.environ[\"AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_DATA\"]\n",
"else:\n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_DATA was not defined. Exiting\\n') \n",
"env_var = \"AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_LOGS\" in os.environ\n",
"if env_var:\n",
" postgres_storage_class_logs = os.environ[\"AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_LOGS\"]\n",
"else:\n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_LOGS was not defined. Exiting\\n') \n",
"env_var = \"AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_BACKUPS\" in os.environ\n",
"if env_var:\n",
" postgres_storage_class_backups = os.environ[\"AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_BACKUPS\"]\n",
"else:\n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_BACKUPS was not defined. Exiting\\n') \n",
""
],
"metadata": {
"azdata_cell_guid": "53769960-e1f8-4477-b4cf-3ab1ea34348b",
"tags": []
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"#### **Get optional parameters for the PostgreSQL server group**"
],
"metadata": {
"azdata_cell_guid": "68ec0760-27d1-4ded-9a9f-89077c40b8bb"
}
},
{
"cell_type": "code",
"source": [
"server_group_workers = os.environ[\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_WORKERS\"]\n",
"server_group_port = os.environ.get(\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_PORT\")\n",
"server_group_cores_request = os.environ.get(\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_REQUEST\")\n",
"server_group_cores_limit = os.environ.get(\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_LIMIT\")\n",
"server_group_memory_request = os.environ.get(\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_REQUEST\")\n",
"server_group_memory_limit = os.environ.get(\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_LIMIT\")"
],
"metadata": {
"azdata_cell_guid": "53769960-e1f8-4477-b4cf-3ab1ea34348b",
"tags": []
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### **Installing PostgreSQL server group**"
"### **Creating the PostgreSQL Hyperscale - Azure Arc instance**"
],
"metadata": {
"azdata_cell_guid": "90b0e162-2987-463f-9ce6-12dda1267189"
@@ -179,7 +113,7 @@
"source": [
"# Login to the data controller.\n",
"#\n",
"os.environ[\"AZDATA_PASSWORD\"] = controller_password\n",
"os.environ[\"AZDATA_PASSWORD\"] = os.environ[\"AZDATA_NB_VAR_CONTROLLER_PASSWORD\"]\n",
"cmd = f'azdata login -e {controller_endpoint} -u {controller_username}'\n",
"out=run_command()"
],
@@ -192,17 +126,22 @@
{
"cell_type": "code",
"source": [
"print (f'Creating a PostgreSQL server group on Azure Arc')\n",
"print (f'Creating the PostgreSQL Hyperscale - Azure Arc instance')\n",
"\n",
"workers_option = f' -w {server_group_workers}' if server_group_workers else \"\"\n",
"port_option = f' --port \"{server_group_port}\"' if server_group_port else \"\"\n",
"cores_request_option = f' -cr \"{server_group_cores_request}\"' if server_group_cores_request else \"\"\n",
"cores_limit_option = f' -cl \"{server_group_cores_limit}\"' if server_group_cores_limit else \"\"\n",
"memory_request_option = f' -mr \"{server_group_memory_request}Mi\"' if server_group_memory_request else \"\"\n",
"memory_limit_option = f' -ml \"{server_group_memory_limit}Mi\"' if server_group_memory_limit else \"\"\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 \"{postgres_server_group_cores_request}\"' if postgres_server_group_cores_request else \"\"\n",
"cores_limit_option = f' -cl \"{postgres_server_group_cores_limit}\"' if postgres_server_group_cores_limit else \"\"\n",
"memory_request_option = f' -mr \"{postgres_server_group_memory_request}Gi\"' if postgres_server_group_memory_request else \"\"\n",
"memory_limit_option = f' -ml \"{postgres_server_group_memory_limit}Gi\"' if postgres_server_group_memory_limit else \"\"\n",
"\n",
"os.environ[\"AZDATA_PASSWORD\"] = postgres_password\n",
"cmd = f'azdata arc postgres server create -n {server_group_name} -scd {postgres_storage_class_data} -scl {postgres_storage_class_logs} -scb {postgres_storage_class_backups}{workers_option}{port_option}{cores_request_option}{cores_limit_option}{memory_request_option}{memory_limit_option}'\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",
"out=run_command()"
],
"metadata": {

View File

@@ -25,12 +25,12 @@
"source": [
"![Microsoft](https://raw.githubusercontent.com/microsoft/azuredatastudio/main/extensions/arc/images/microsoft-small-logo.png)\n",
" \n",
"## Deploy Azure SQL managed instance on an existing Azure Arc data cluster\n",
"## Create SQL managed instance - Azure Arc on an existing Azure Arc Data Controller\n",
" \n",
"This notebook walks through the process of deploying a <a href=\"https://docs.microsoft.com/azure/sql-database/sql-database-managed-instance\">Azure SQL managed instance</a> on an existing Azure Arc data cluster.\n",
"This notebook walks through the process of creating a <a href=\"https://docs.microsoft.com/azure/sql-database/sql-database-managed-instance\">SQL managed instance - Azure Arc</a> on an existing Azure Arc Data Controller.\n",
" \n",
"* Follow the instructions in the **Prerequisites** cell to install the tools if not already installed.\n",
"* Make sure you have the target Azure Arc data cluster already created.\n",
"* Make sure you have the target Azure Arc Data Controller already created.\n",
"\n",
"<span style=\"color:red\"><font size=\"3\">Please press the \"Run All\" button to run the notebook</font></span>"
],
@@ -41,7 +41,21 @@
{
"cell_type": "markdown",
"source": [
"### **Check prerequisites**"
"### **Prerequisites** \n",
"Ensure the following tools are installed and added to PATH before proceeding.\n",
" \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) |"
],
"metadata": {
"azdata_cell_guid": "d1c8258e-9efd-4380-a48c-cd675423ed2f"
}
},
{
"cell_type": "markdown",
"source": [
"### **Setup and Check Prerequisites**"
],
"metadata": {
"azdata_cell_guid": "68531b91-ddce-47d7-a1d8-2ddc3d17f3e7"
@@ -75,70 +89,20 @@
{
"cell_type": "markdown",
"source": [
"#### **Ensure SQL instance name, username and password exist**"
"### **Set variables**\n",
"\n",
"#### \n",
"\n",
"Generated by Azure Data Studio using the values collected in the 'Deploy Azure SQL managed instance - Azure Arc' wizard"
],
"metadata": {
"azdata_cell_guid": "68ec0760-27d1-4ded-9a9f-89077c40b8bb"
}
},
{
"cell_type": "code",
"source": [
"# Required Values\n",
"env_var = \"AZDATA_NB_VAR_CONTROLLER_ENDPOINT\" in os.environ\n",
"if env_var:\n",
" controller_endpoint = os.environ[\"AZDATA_NB_VAR_CONTROLLER_ENDPOINT\"]\n",
"else:\n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_CONTROLLER_ENDPOINT was not defined. Exiting\\n')\n",
"\n",
"env_var = \"AZDATA_NB_VAR_CONTROLLER_USERNAME\" in os.environ\n",
"if env_var:\n",
" controller_username = os.environ[\"AZDATA_NB_VAR_CONTROLLER_USERNAME\"]\n",
"else:\n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_CONTROLLER_USERNAME was not defined. Exiting\\n')\n",
"\n",
"env_var = \"AZDATA_NB_VAR_CONTROLLER_PASSWORD\" in os.environ\n",
"if env_var:\n",
" controller_password = os.environ[\"AZDATA_NB_VAR_CONTROLLER_PASSWORD\"]\n",
"else:\n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_CONTROLLER_PASSWORD was not defined. Exiting\\n')\n",
"\n",
"env_var = \"AZDATA_NB_VAR_SQL_INSTANCE_NAME\" in os.environ\n",
"if env_var:\n",
" mssql_instance_name = os.environ[\"AZDATA_NB_VAR_SQL_INSTANCE_NAME\"]\n",
"else:\n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_SQL_INSTANCE_NAME was not defined. Exiting\\n')\n",
"\n",
"env_var = \"AZDATA_NB_VAR_SQL_PASSWORD\" in os.environ\n",
"if env_var:\n",
" mssql_password = os.environ[\"AZDATA_NB_VAR_SQL_PASSWORD\"]\n",
"else:\n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_SQL_PASSWORD was not defined. Exiting\\n')\n",
"\n",
"env_var = \"AZDATA_NB_VAR_SQL_STORAGE_CLASS_DATA\" in os.environ\n",
"if env_var:\n",
" mssql_storage_class_data = os.environ[\"AZDATA_NB_VAR_SQL_STORAGE_CLASS_DATA\"]\n",
"else:\n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_SQL_STORAGE_CLASS_DATA was not defined. Exiting\\n')\n",
"\n",
"env_var = \"AZDATA_NB_VAR_SQL_STORAGE_CLASS_LOGS\" in os.environ\n",
"if env_var:\n",
" mssql_storage_class_logs = os.environ[\"AZDATA_NB_VAR_SQL_STORAGE_CLASS_LOGS\"]\n",
"else:\n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_SQL_STORAGE_CLASS_LOGS was not defined. Exiting\\n') \n",
""
],
"metadata": {
"azdata_cell_guid": "53769960-e1f8-4477-b4cf-3ab1ea34348b",
"tags": []
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### **Installing Managed SQL Instance**"
"### **Creating the SQL managed instance - Azure Arc instance**"
],
"metadata": {
"azdata_cell_guid": "90b0e162-2987-463f-9ce6-12dda1267189"
@@ -149,7 +113,7 @@
"source": [
"# Login to the data controller.\n",
"#\n",
"os.environ[\"AZDATA_PASSWORD\"] = controller_password\n",
"os.environ[\"AZDATA_PASSWORD\"] = os.environ[\"AZDATA_NB_VAR_CONTROLLER_PASSWORD\"]\n",
"cmd = f'azdata login -e {controller_endpoint} -u {controller_username}'\n",
"out=run_command()"
],
@@ -162,10 +126,16 @@
{
"cell_type": "code",
"source": [
"print (f'Creating Managed SQL Server instance on Azure Arc')\n",
"print (f'Creating the SQL managed instance - Azure Arc instance')\n",
"\n",
"os.environ[\"AZDATA_PASSWORD\"] = mssql_password\n",
"cmd = f'azdata arc sql mi create -n {mssql_instance_name} -scd {mssql_storage_class_data} -scl {mssql_storage_class_logs}'\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",
"\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}{cores_request_option}{cores_limit_option}{memory_request_option}{memory_limit_option}'\n",
"out=run_command()"
],
"metadata": {

View File

@@ -2,7 +2,7 @@
"name": "arc",
"displayName": "%arc.displayName%",
"description": "%arc.description%",
"version": "0.3.5",
"version": "0.5.1",
"publisher": "Microsoft",
"preview": true,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
@@ -14,6 +14,7 @@
"activationEvents": [
"onCommand:arc.connectToController",
"onCommand:arc.createController",
"onCommand:azdata.resource.deploy",
"onView:azureArc"
],
"extensionDependencies": [
@@ -97,7 +98,7 @@
"view/item/context": [
{
"command": "arc.openDashboard",
"when": "view == azureArc && viewItem != postgresInstances",
"when": "view == azureArc && viewItem",
"group": "navigation@1"
},
{
@@ -143,7 +144,7 @@
"providers": [
{
"notebookWizard": {
"notebook": "./notebooks/arcDeployment/deploy.arc.control.plane.ipynb",
"notebook": "./notebooks/arcDeployment/deploy.arc.data.controller.ipynb",
"type": "new-arc-control-plane",
"doneAction": {
"label": "%deploy.done.action%"
@@ -158,52 +159,30 @@
"generateSummaryPage": false,
"pages": [
{
"title": "%arc.control.plane.select.cluster.title%",
"title": "%arc.data.controller.select.cluster.title%",
"sections": [
{
"fields": [
{
"type": "kube_cluster_context_picker",
"label": "%arc.control.plane.kube.cluster.context%",
"label": "%arc.data.controller.kube.cluster.context%",
"required": true,
"inputWidth": "350px",
"variableName": "AZDATA_NB_VAR_ARC_CLUSTER_CONTEXT",
"configFileVariableName": "AZDATA_NB_VAR_ARC_CONFIG_FILE"
}
]
},
{
"title": "%arc.control.plane.container.registry.title%",
"fields": [
{
"label": "%arc.control.plane.container.registry.name%",
"variableName": "AZDATA_NB_VAR_ARC_DOCKER_USERNAME",
"type": "text",
"required": true,
"defaultValue": "22cda7bb-2eb1-419e-a742-8710c313fe79",
"enabled": true
},
{
"label": "%arc.control.plane.container.registry.password%",
"variableName": "AZDATA_NB_VAR_ARC_DOCKER_PASSWORD",
"type": "password",
"userName": "docker",
"confirmationRequired": false,
"defaultValue": "",
"required": true
}
]
}
]
},
{
"title": "%arc.control.plane.cluster.config.profile.title%",
"title": "%arc.data.controller.cluster.config.profile.title%",
"sections": [
{
"fields": [
{
"type": "options",
"label": "%arc.control.plane.cluster.config.profile%",
"label": "%arc.data.controller.cluster.config.profile%",
"required": true,
"variableName": "AZDATA_NB_VAR_ARC_PROFILE",
"editable": false,
@@ -220,14 +199,14 @@
]
},
{
"title": "%arc.control.plane.data.controller.create.title%",
"title": "%arc.data.controller.data.controller.create.title%",
"sections": [
{
"title": "%arc.control.plane.project.details.title%",
"title": "%arc.data.controller.project.details.title%",
"fields": [
{
"type": "readonly_text",
"label": "%arc.control.plane.project.details.description%",
"label": "%arc.data.controller.project.details.description%",
"labelWidth": "600px"
},
{
@@ -240,30 +219,30 @@
]
},
{
"title": "%arc.control.plane.data.controller.details.title%",
"title": "%arc.data.controller.data.controller.details.title%",
"fields": [
{
"type": "readonly_text",
"label": "%arc.control.plane.data.controller.details.description%",
"label": "%arc.data.controller.data.controller.details.description%",
"labelWidth": "600px"
},
{
"type": "text",
"label": "%arc.control.plane.arc.data.controller.namespace%",
"label": "%arc.data.controller.arc.data.controller.namespace%",
"textValidationRequired": true,
"textValidationRegex": "^[a-z0-9]([-a-z0-9]{0,11}[a-z0-9])?$",
"textValidationDescription": "%arc.control.plane.arc.data.controller.namespace.validation.description%",
"textValidationRegex": "^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?$",
"textValidationDescription": "%arc.data.controller.arc.data.controller.namespace.validation.description%",
"defaultValue": "arc",
"required": true,
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAMESPACE"
},
{
"type": "text",
"label": "%arc.control.plane.arc.data.controller.name%",
"label": "%arc.data.controller.arc.data.controller.name%",
"textValidationRequired": true,
"textValidationRegex": "^[a-z0-9]([-a-z0-9]{0,11}[a-z0-9])?$",
"textValidationDescription": "%arc.control.plane.arc.data.controller.name.validation.description%",
"defaultValue": "arc-cp1",
"textValidationRegex": "^[a-z0-9]([-.a-z0-9]{0,251}[a-z0-9])?$",
"textValidationDescription": "%arc.data.controller.arc.data.controller.name.validation.description%",
"defaultValue": "arc-dc",
"required": true,
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAME"
},
@@ -276,42 +255,33 @@
},
{
"type": "azure_locations",
"label": "%arc.control.plane.arc.data.controller.location%",
"label": "%arc.data.controller.arc.data.controller.location%",
"defaultValue": "eastus",
"required": true,
"locationVariableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_LOCATION",
"displayLocationVariableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_DISPLAY_LOCATION",
"locations": [
"australiaeast",
"centralus",
"eastus",
"eastus2",
"centralus",
"westus2",
"francecentral",
"japaneast",
"koreacentral",
"northeurope",
"southeastasia",
"westeurope"
"uksouth",
"westeurope",
"westus2"
]
},
{
"type": "options",
"label": "%arc.control.plane.arc.data.controller.connectivity.mode%",
"options": {
"values": [
"Indirect",
"Direct"
],
"defaultValue": "Indirect",
"optionsType": "radio"
},
"enabled": false,
"required": true,
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_CONNECTIVITY_MODE"
}
]
},
{
"title": "%arc.control.plane.admin.account.title%",
"title": "%arc.data.controller.admin.account.title%",
"fields": [
{
"label": "%arc.control.plane.admin.account.name%",
"label": "%arc.data.controller.admin.account.name%",
"variableName": "AZDATA_NB_VAR_ARC_ADMIN_USERNAME",
"type": "text",
"required": true,
@@ -319,12 +289,12 @@
"enabled": true
},
{
"label": "%arc.control.plane.admin.account.password%",
"label": "%arc.data.controller.admin.account.password%",
"variableName": "AZDATA_NB_VAR_ARC_ADMIN_PASSWORD",
"type": "sql_password",
"userName": "arcadmin",
"confirmationRequired": true,
"confirmationLabel": "%arc.control.plane.admin.account.confirm.password%",
"confirmationLabel": "%arc.data.controller.admin.account.confirm.password%",
"defaultValue": "",
"required": true
}
@@ -333,7 +303,7 @@
]
},
{
"title": "%arc.control.plane.data.controller.create.summary.title%",
"title": "%arc.data.controller.data.controller.create.summary.title%",
"isSummaryPage": true,
"fieldHeight": "16px",
"sections": [
@@ -349,7 +319,7 @@
{
"items": [
{
"label": "%arc.control.plane.summary.arc.data.controller%",
"label": "%arc.data.controller.summary.arc.data.controller%",
"type": "readonly_text",
"enabled": true,
"labelWidth": "185px"
@@ -359,7 +329,7 @@
{
"items": [
{
"label": "%arc.control.plane.summary.estimated.cost.per.month%",
"label": "%arc.data.controller.summary.estimated.cost.per.month%",
"type": "readonly_text",
"enabled": true,
"labelWidth": "190px",
@@ -376,7 +346,7 @@
{
"items": [
{
"label": "%arc.control.plane.summary.arc.by.microsoft%",
"label": "%arc.data.controller.summary.arc.by.microsoft%",
"type": "readonly_text",
"labelWidth": "185px"
}
@@ -385,7 +355,7 @@
{
"items": [
{
"label": "%arc.control.plane.summary.free%",
"label": "%arc.data.controller.summary.free%",
"type": "readonly_text",
"enabled": true,
"defaultValue": "",
@@ -403,10 +373,10 @@
"label": "{0}",
"type": "readonly_text",
"enabled": true,
"labelWidth": "67px",
"labelWidth": "69px",
"links": [
{
"text": "%arc.control.plane.summary.arc.terms.of.use%",
"text": "%arc.data.controller.summary.arc.terms.of.use%",
"url": "https://go.microsoft.com/fwlink/?linkid=2045708"
}
]
@@ -423,10 +393,10 @@
"label": "{0}",
"type": "readonly_text",
"enabled": true,
"labelWidth": "102px",
"labelWidth": "100px",
"links": [
{
"text": "%arc.control.plane.summary.arc.terms.privacy.policy%",
"text": "%arc.data.controller.summary.arc.terms.privacy.policy%",
"url": "https://go.microsoft.com/fwlink/?linkid=512132"
}
]
@@ -438,17 +408,17 @@
]
},
{
"title": "%arc.control.plane.summary.terms%",
"title": "%arc.data.controller.summary.terms%",
"fieldHeight": "88px",
"fields": [
{
"label": "%arc.control.plane.summary.terms.description%",
"label": "%arc.data.controller.summary.terms.description%",
"type": "readonly_text",
"enabled": true,
"labelWidth": "750px",
"links": [
{
"text": "%arc.control.plane.summary.terms.link.text%",
"text": "%arc.data.controller.summary.terms.link.text%",
"url": "https://go.microsoft.com/fwlink/?linkid=2045624"
}
]
@@ -456,76 +426,64 @@
]
},
{
"title": "%arc.control.plane.summary.kubernetes%",
"title": "%arc.data.controller.summary.kubernetes%",
"fields": [
{
"label": "%arc.control.plane.summary.kube.config.file.path%",
"label": "%arc.data.controller.summary.kube.config.file.path%",
"type": "readonly_text",
"isEvaluated": true,
"defaultValue": "$(AZDATA_NB_VAR_ARC_CONFIG_FILE)"
},
{
"label": "%arc.control.plane.summary.cluster.context%",
"label": "%arc.data.controller.summary.cluster.context%",
"type": "readonly_text",
"isEvaluated": true,
"defaultValue": "$(AZDATA_NB_VAR_ARC_CLUSTER_CONTEXT)"
},
{
"label": "%arc.control.plane.summary.profile%",
"label": "%arc.data.controller.summary.profile%",
"type": "readonly_text",
"isEvaluated": true,
"defaultValue": "$(AZDATA_NB_VAR_ARC_PROFILE)"
},
{
"label": "%arc.control.plane.summary.username%",
"label": "%arc.data.controller.summary.username%",
"type": "readonly_text",
"isEvaluated": true,
"defaultValue": "$(AZDATA_NB_VAR_ARC_ADMIN_USERNAME)"
},
{
"label": "%arc.control.plane.summary.docker.username%",
"type": "readonly_text",
"isEvaluated": true,
"defaultValue": "$(AZDATA_NB_VAR_ARC_DOCKER_USERNAME)"
}
]
},
{
"title": "%arc.control.plane.summary.azure%",
"title": "%arc.data.controller.summary.azure%",
"fields": [
{
"label": "%arc.control.plane.summary.data.controller.namespace%",
"label": "%arc.data.controller.summary.data.controller.namespace%",
"type": "readonly_text",
"isEvaluated": true,
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAMESPACE)"
},
{
"label": "%arc.control.plane.summary.data.controller.name%",
"label": "%arc.data.controller.summary.data.controller.name%",
"type": "readonly_text",
"isEvaluated": true,
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAME)"
},
{
"label": "%arc.control.plane.summary.data.controller.connectivity.mode%",
"type": "readonly_text",
"isEvaluated": true,
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_CONNECTIVITY_MODE)"
},
{
"label": "%arc.control.plane.summary.subscription%",
"label": "%arc.data.controller.summary.subscription%",
"type": "readonly_text",
"isEvaluated": true,
"defaultValue": "$(AZDATA_NB_VAR_ARC_DISPLAY_SUBSCRIPTION)",
"inputWidth": "600"
},
{
"label": "%arc.control.plane.summary.resource.group%",
"label": "%arc.data.controller.summary.resource.group%",
"type": "readonly_text",
"isEvaluated": true,
"defaultValue": "$(AZDATA_NB_VAR_ARC_RESOURCE_GROUP)"
},
{
"label": "%arc.control.plane.summary.location%",
"label": "%arc.data.controller.summary.location%",
"type": "readonly_text",
"isEvaluated": true,
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_DISPLAY_LOCATION)"
@@ -542,7 +500,7 @@
},
{
"name": "azdata",
"version": "20.1.0"
"version": "20.2.0"
}
],
"when": true
@@ -561,7 +519,7 @@
"tags": ["Hybrid", "SQL Server"],
"providers": [
{
"dialog": {
"notebookWizard": {
"notebook": "./notebooks/arcDeployment/deploy.sql.existing.arc.ipynb",
"doneAction": {
"label": "%deploy.done.action%"
@@ -576,14 +534,16 @@
"generateSummaryPage": false,
"pages": [
{
"title": "",
"title": "%arc.sql.wizard.page1.title%",
"labelWidth": "175px",
"inputWidth": "280px",
"sections": [
{
"title": "%arc.sql.settings.section.title%",
"title": "%arc.sql.connection.settings.section.title%",
"fields": [
{
"label": "%arc.controller%",
"variableName": "AZDATA_NB_VAR_ARC_CONTROLLER",
"variableName": "",
"type": "options",
"editable": false,
"required": true,
@@ -597,23 +557,28 @@
}
},
"optionsType": "dropdown"
},
"labelWidth": "100%"
}
},
{
"label": "%arc.sql.instance.name%",
"variableName": "AZDATA_NB_VAR_SQL_INSTANCE_NAME",
"type": "text",
"defaultValue": "sqlinstance1",
"description": "%arc.sql.invalid.instance.name%",
"required": true,
"labelWidth": "100%"
"textValidationRequired": true,
"textValidationRegex": "^[a-z]([-a-z0-9]{0,11}[a-z0-9])?$",
"textValidationDescription": "%arc.sql.invalid.instance.name%"
},
{
"label": "%arc.sql.username%",
"variableName": "AZDATA_NB_VAR_SQL_USERNAME",
"type": "text",
"defaultValue": "sa",
"enabled": false
"description": "%arc.sql.invalid.username%",
"required": true,
"textValidationRequired": true,
"textValidationRegex": "^(?!sa$)",
"textValidationDescription": "%arc.sql.invalid.username%"
},
{
"label": "%arc.password%",
@@ -624,7 +589,12 @@
"confirmationLabel": "%arc.confirm.password%",
"defaultValue": "",
"required": true
}
]
},
{
"title": "%arc.sql.instance.settings.section.title%",
"fields": [
{
"label": "%arc.storage-class.data.label%",
"description": "%arc.sql.storage-class.data.description%",
@@ -638,6 +608,38 @@
"variableName": "AZDATA_NB_VAR_SQL_STORAGE_CLASS_LOGS",
"type": "kube_storage_class",
"required": true
},
{
"label": "%arc.cores-request.label%",
"description": "%arc.sql.cores-request.description%",
"variableName": "AZDATA_NB_VAR_SQL_CORES_REQUEST",
"type": "number",
"min": 1,
"required": false
},
{
"label": "%arc.cores-limit.label%",
"description": "%arc.sql.cores-limit.description%",
"variableName": "AZDATA_NB_VAR_SQL_CORES_LIMIT",
"type": "number",
"min": 1,
"required": false
},
{
"label": "%arc.memory-request.label%",
"description": "%arc.sql.memory-request.description%",
"variableName": "AZDATA_NB_VAR_SQL_MEMORY_REQUEST",
"type": "number",
"min": 2,
"required": false
},
{
"label": "%arc.memory-limit.label%",
"description": "%arc.sql.memory-limit.description%",
"variableName": "AZDATA_NB_VAR_SQL_MEMORY_LIMIT",
"type": "number",
"min": 2,
"required": false
}
]
}
@@ -651,7 +653,7 @@
},
{
"name": "azdata",
"version": "20.1.0"
"version": "20.2.0"
}
],
"when": "true"
@@ -683,7 +685,7 @@
"tags": ["Hybrid", "PostgreSQL"],
"providers": [
{
"dialog": {
"notebookWizard": {
"notebook": "./notebooks/arcDeployment/deploy.postgres.existing.arc.ipynb",
"doneAction": {
"label": "%deploy.done.action%"
@@ -698,14 +700,16 @@
"generateSummaryPage": false,
"pages": [
{
"title": "",
"title": "%arc.postgres.wizard.page1.title%",
"labelWidth": "205px",
"inputWidth": "280px",
"sections": [
{
"title": "%arc.postgres.settings.section.title%",
"fields": [
{
"label": "%arc.controller%",
"variableName": "AZDATA_NB_VAR_ARC_CONTROLLER",
"variableName": "",
"type": "options",
"editable": false,
"required": true,
@@ -719,17 +723,16 @@
}
},
"optionsType": "dropdown"
},
"labelWidth": "100%"
}
},
{
"label": "%arc.postgres.server.group.name%",
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_NAME",
"type": "text",
"description": "%arc.postgres.server.group.name.validation.description%",
"textValidationRequired": true,
"textValidationRegex": "^[a-z]([-a-z0-9]{0,8}[a-z0-9])?$",
"textValidationRegex": "^[a-z]([-a-z0-9]{0,10}[a-z0-9])?$",
"textValidationDescription": "%arc.postgres.server.group.name.validation.description%",
"defaultValue": "postgres1",
"required": true
},
{
@@ -742,11 +745,12 @@
"required": true
},
{
"label": "%arc.postgres.server.group.workers%",
"label": "%arc.postgres.server.group.workers.label%",
"description": "%arc.postgres.server.group.workers.description%",
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_WORKERS",
"type": "number",
"defaultValue": "1",
"min": 1
"defaultValue": "0",
"min": 0
},
{
"label": "%arc.postgres.server.group.port%",
@@ -756,6 +760,27 @@
"min": 1,
"max": 65535
},
{
"label": "%arc.postgres.server.group.engine.version%",
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_ENGINE_VERSION",
"type": "options",
"options": [
"11",
"12"
],
"defaultValue": "12"
},
{
"label": "%arc.postgres.server.group.extensions.label%",
"description": "%arc.postgres.server.group.extensions.description%",
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_EXTENSIONS",
"type": "text"
}
]
},
{
"title": "%arc.postgres.settings.storage.title%",
"fields": [
{
"label": "%arc.storage-class.data.label%",
"description": "%arc.postgres.storage-class.data.description%",
@@ -763,6 +788,14 @@
"type": "kube_storage_class",
"required": true
},
{
"label": "%arc.postgres.server.group.volume.size.data.label%",
"description": "%arc.postgres.server.group.volume.size.data.description%",
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_VOLUME_SIZE_DATA",
"type": "number",
"defaultValue": "5",
"min": 1
},
{
"label": "%arc.storage-class.logs.label%",
"description": "%arc.postgres.storage-class.logs.description%",
@@ -770,12 +803,28 @@
"type": "kube_storage_class",
"required": true
},
{
"label": "%arc.postgres.server.group.volume.size.logs.label%",
"description": "%arc.postgres.server.group.volume.size.logs.description%",
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_VOLUME_SIZE_LOGS",
"type": "number",
"defaultValue": "5",
"min": 1
},
{
"label": "%arc.storage-class.backups.label%",
"description": "%arc.postgres.storage-class.backups.description%",
"variableName": "AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_BACKUPS",
"type": "kube_storage_class",
"required": true
},
{
"label": "%arc.postgres.server.group.volume.size.backups.label%",
"description": "%arc.postgres.server.group.volume.size.backups.description%",
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_VOLUME_SIZE_BACKUPS",
"type": "number",
"defaultValue": "5",
"min": 1
}
]
},
@@ -783,28 +832,32 @@
"title": "%arc.postgres.settings.resource.title%",
"fields": [
{
"label": "%arc.postgres.server.group.cores.request%",
"label": "%arc.postgres.server.group.cores.request.label%",
"description": "%arc.postgres.server.group.cores.request.description%",
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_REQUEST",
"type": "number",
"min": 0
"min": 1
},
{
"label": "%arc.postgres.server.group.cores.limit%",
"label": "%arc.postgres.server.group.cores.limit.label%",
"description": "%arc.postgres.server.group.cores.limit.description%",
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_LIMIT",
"type": "number",
"min": 0
"min": 1
},
{
"label": "%arc.postgres.server.group.memory.request%",
"label": "%arc.postgres.server.group.memory.request.label%",
"description": "%arc.postgres.server.group.memory.request.description%",
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_REQUEST",
"type": "number",
"min": 0
"min": 0.25
},
{
"label": "%arc.postgres.server.group.memory.limit%",
"label": "%arc.postgres.server.group.memory.limit.label%",
"description": "%arc.postgres.server.group.memory.limit.description%",
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_LIMIT",
"type": "number",
"min": 0
"min": 0.25
}
]
}
@@ -814,11 +867,11 @@
},
"requiredTools": [
{
"name": "azure-cli"
"name": "kubectl"
},
{
"name": "azdata",
"version": "20.1.0"
"version": "20.2.0"
}
],
"when": "true"
@@ -858,5 +911,10 @@
"sinon": "^9.0.2",
"typemoq": "2.1.0",
"vscodetestcover": "^1.1.0"
},
"__metadata": {
"id": "68",
"publisherDisplayName": "Microsoft",
"publisherId": "Microsoft"
}
}

View File

@@ -148,15 +148,15 @@ async function promptInputBox(title: string, options: vscode.InputBoxOptions): P
}
/**
* Opens an input box prompting the user to enter in the name of a resource to delete
* @param name The name of the resource to delete
* Opens an input box prompting the user to enter in the name of an instance to delete
* @param name The name of the instance to delete
* @returns Promise resolving to true if the user confirmed the name, false if the input box was closed for any other reason
*/
export async function promptForResourceDeletion(name: string): Promise<boolean> {
const title = loc.resourceDeletionWarning(name);
export async function promptForInstanceDeletion(name: string): Promise<boolean> {
const title = loc.instanceDeletionWarning(name);
const options: vscode.InputBoxOptions = {
placeHolder: name,
validateInput: input => input !== name ? loc.invalidResourceDeletionName(name) : ''
validateInput: input => input !== name ? loc.invalidInstanceDeletionName(name) : ''
};
return await promptInputBox(title, options) !== undefined;
@@ -189,28 +189,15 @@ export async function promptAndConfirmPassword(validate: (input: string) => stri
/**
* Gets the message to display for a given error object that may be a variety of types.
* @param error The error object
* @param useMessageWithLink Whether to use the messageWithLink - if available
*/
export function getErrorMessage(error: any): string {
export function getErrorMessage(error: any, useMessageWithLink: boolean = false): string {
if (useMessageWithLink && error.messageWithLink) {
return error.messageWithLink;
}
return error.message ?? error;
}
/**
* Parses an instance name from the controller. An instance name will either be just its name
* e.g. myinstance or namespace_name e.g. mynamespace_my-instance.
* @param instanceName The instance name in one of the formats described
*/
export function parseInstanceName(instanceName: string | undefined): string {
instanceName = instanceName ?? '';
const parts: string[] = instanceName.split('_');
if (parts.length === 2) {
instanceName = parts[1];
}
else if (parts.length > 2) {
throw new Error(`Cannot parse resource '${instanceName}'. Acceptable formats are 'namespace_name' or 'name'.`);
}
return instanceName;
}
/**
* Parses an address into its separate ip and port values. Address must be in the form <ip>:<port>
* @param address The address to parse

View File

@@ -7,6 +7,11 @@ import * as vscode from 'vscode';
export const refreshActionId = 'arc.refresh';
export const credentialNamespace = 'arcCredentials';
export const controllerTroubleshootDocsUrl = 'https://aka.ms/arc-data-tsg';
export const miaaTroubleshootDocsUrl = 'https://aka.ms/miaa-tsg';
export interface IconPath {
dark: string;
light: string;

View File

@@ -28,6 +28,14 @@ export async function activate(context: vscode.ExtensionContext): Promise<arc.IE
});
vscode.commands.registerCommand('arc.connectToController', async () => {
const nodes = await treeDataProvider.getChildren();
if (nodes.length > 0) {
const response = await vscode.window.showErrorMessage(loc.onlyOneControllerSupported, loc.yes, loc.no);
if (response !== loc.yes) {
return;
}
await treeDataProvider.removeController(nodes[0] as ControllerTreeNode);
}
const dialog = new ConnectToControllerDialog(treeDataProvider);
dialog.showDialog();
const model = await dialog.waitForClose();

View File

@@ -8,13 +8,13 @@ import { getErrorMessage } from './common/utils';
const localize = nls.loadMessageBundle();
export const arcDeploymentDeprecation = localize('arc.arcDeploymentDeprecation', "The Arc Deployment extension has been replaced by the Arc extension and has been uninstalled.");
export function arcControllerDashboard(name: string): string { return localize('arc.controllerDashboard', "Azure Arc Controller Dashboard (Preview) - {0}", name); }
export function miaaDashboard(name: string): string { return localize('arc.miaaDashboard', "Managed Instance Dashboard (Preview) - {0}", name); }
export function postgresDashboard(name: string): string { return localize('arc.postgresDashboard', "Postgres Dashboard (Preview) - {0}", name); }
export function arcControllerDashboard(name: string): string { return localize('arc.controllerDashboard', "Azure Arc Data Controller Dashboard (Preview) - {0}", name); }
export function miaaDashboard(name: string): string { return localize('arc.miaaDashboard', "SQL managed instance - Azure Arc Dashboard (Preview) - {0}", name); }
export function postgresDashboard(name: string): string { return localize('arc.postgresDashboard', "PostgreSQL Hyperscale - Azure Arc Dashboard (Preview) - {0}", name); }
export const dataControllersType = localize('arc.dataControllersType', "Azure Arc Data Controller");
export const pgSqlType = localize('arc.pgSqlType', "PostgreSQL Server group - Azure Arc");
export const miaaType = localize('arc.miaaType', "SQL instance - Azure Arc");
export const pgSqlType = localize('arc.pgSqlType', "PostgreSQL Hyperscale - Azure Arc");
export const miaaType = localize('arc.miaaType', "SQL managed instance - Azure Arc");
export const overview = localize('arc.overview', "Overview");
export const connectionStrings = localize('arc.connectionStrings', "Connection Strings");
@@ -72,9 +72,12 @@ export const direct = localize('arc.direct', "Direct");
export const indirect = localize('arc.indirect', "Indirect");
export const loading = localize('arc.loading', "Loading...");
export const refreshToEnterCredentials = localize('arc.refreshToEnterCredentials', "Refresh node to enter credentials");
export const noInstancesAvailable = localize('arc.noInstancesAvailable', "No instances available");
export const connectToController = localize('arc.connectToController', "Connect to Existing Controller");
export function connectToSql(name: string): string { return localize('arc.connectToSql', "Connect to SQL managed instance - Azure Arc ({0})", name); }
export const passwordToController = localize('arc.passwordToController', "Provide Password to Controller");
export const controllerUrl = localize('arc.controllerUrl', "Controller URL");
export const serverEndpoint = localize('arc.serverEndpoint', "Server Endpoint");
export const controllerName = localize('arc.controllerName', "Name");
export const defaultControllerName = localize('arc.defaultControllerName', "arc-dc");
export const username = localize('arc.username', "Username");
@@ -123,9 +126,11 @@ export const condition = localize('arc.condition', "Condition");
export const details = localize('arc.details', "Details");
export const lastUpdated = localize('arc.lastUpdated', "Last updated");
export const noExternalEndpoint = localize('arc.noExternalEndpoint', "No External Endpoint has been configured so this information isn't available.");
export const podsReady = localize('arc.podsReady', "pods ready");
export function databaseCreated(name: string): string { return localize('arc.databaseCreated', "Database {0} created", name); }
export function resourceDeleted(name: string): string { return localize('arc.resourceDeleted', "Resource '{0}' deleted", name); }
export function deletingInstance(name: string): string { return localize('arc.deletingInstance', "Deleting instance '{0}'...", name); }
export function instanceDeleted(name: string): string { return localize('arc.instanceDeleted', "Instance '{0}' deleted", name); }
export function copiedToClipboard(name: string): string { return localize('arc.copiedToClipboard', "{0} copied to clipboard", name); }
export function clickTheTroubleshootButton(resourceType: string): string { return localize('arc.clickTheTroubleshootButton', "Click the troubleshoot button to open the Azure Arc {0} troubleshooting notebook.", resourceType); }
export function numVCores(vCores: string | undefined): string {
@@ -146,18 +151,19 @@ export const connectionRequired = localize('arc.connectionRequired', "A connecti
export const couldNotFindControllerRegistration = localize('arc.couldNotFindControllerRegistration', "Could not find controller registration.");
export function refreshFailed(error: any): string { return localize('arc.refreshFailed', "Refresh failed. {0}", getErrorMessage(error)); }
export function openDashboardFailed(error: any): string { return localize('arc.openDashboardFailed', "Error opening dashboard. {0}", getErrorMessage(error)); }
export function resourceDeletionFailed(name: string, error: any): string { return localize('arc.resourceDeletionFailed', "Failed to delete resource {0}. {1}", name, getErrorMessage(error)); }
export function instanceDeletionFailed(name: string, error: any): string { return localize('arc.instanceDeletionFailed', "Failed to delete instance {0}. {1}", name, getErrorMessage(error)); }
export function databaseCreationFailed(name: string, error: any): string { return localize('arc.databaseCreationFailed', "Failed to create database {0}. {1}", name, getErrorMessage(error)); }
export function connectToControllerFailed(url: string, error: any): string { return localize('arc.connectToControllerFailed', "Could not connect to controller {0}. {1}", url, getErrorMessage(error)); }
export function connectToSqlFailed(serverName: string, error: any): string { return localize('arc.connectToSqlFailed', "Could not connect to SQL managed instance - Azure Arc Instance {0}. {1}", serverName, getErrorMessage(error)); }
export function fetchConfigFailed(name: string, error: any): string { return localize('arc.fetchConfigFailed', "An unexpected error occurred retrieving the config for '{0}'. {1}", name, getErrorMessage(error)); }
export function fetchEndpointsFailed(name: string, error: any): string { return localize('arc.fetchEndpointsFailed', "An unexpected error occurred retrieving the endpoints for '{0}'. {1}", name, getErrorMessage(error)); }
export function fetchRegistrationsFailed(name: string, error: any): string { return localize('arc.fetchRegistrationsFailed', "An unexpected error occurred retrieving the registrations for '{0}'. {1}", name, getErrorMessage(error)); }
export function fetchDatabasesFailed(name: string, error: any): string { return localize('arc.fetchDatabasesFailed', "An unexpected error occurred retrieving the databases for '{0}'. {1}", name, getErrorMessage(error)); }
export function resourceDeletionWarning(name: string): string { return localize('arc.resourceDeletionWarning', "Warning! Deleting a resource is permanent and cannot be undone. To delete the resource '{0}' type the name '{0}' below to proceed.", name); }
export function invalidResourceDeletionName(name: string): string { return localize('arc.invalidResourceDeletionName', "The value '{0}' does not match the instance name. Try again or press escape to exit", name); }
export function instanceDeletionWarning(name: string): string { return localize('arc.instanceDeletionWarning', "Warning! Deleting an instance is permanent and cannot be undone. To delete the instance '{0}' type the name '{0}' below to proceed.", name); }
export function invalidInstanceDeletionName(name: string): string { return localize('arc.invalidInstanceDeletionName', "The value '{0}' does not match the instance name. Try again or press escape to exit", name); }
export function couldNotFindAzureResource(name: string): string { return localize('arc.couldNotFindAzureResource', "Could not find Azure resource for {0}", name); }
export function passwordResetFailed(error: any): string { return localize('arc.passwordResetFailed', "Failed to reset password. {0}", getErrorMessage(error)); }
export function errorConnectingToController(error: any): string { return localize('arc.errorConnectingToController', "Error connecting to controller. {0}", getErrorMessage(error)); }
export function errorConnectingToController(error: any): string { return localize('arc.errorConnectingToController', "Error connecting to controller. {0}", getErrorMessage(error, true)); }
export function passwordAcquisitionFailed(error: any): string { return localize('arc.passwordAcquisitionFailed', "Failed to acquire password. {0}", getErrorMessage(error)); }
export const invalidPassword = localize('arc.invalidPassword', "The password did not work, try again.");
export function errorVerifyingPassword(error: any): string { return localize('arc.errorVerifyingPassword', "Error encountered while verifying password. {0}", getErrorMessage(error)); }

View File

@@ -6,7 +6,7 @@
import { ControllerInfo, ResourceType } from 'arc';
import * as azdataExt from 'azdata-ext';
import * as vscode from 'vscode';
import { parseInstanceName, UserCancelledError } from '../common/utils';
import { UserCancelledError } from '../common/utils';
import * as loc from '../localizedConstants';
import { ConnectToControllerDialog } from '../ui/dialogs/connectControllerDialog';
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
@@ -20,7 +20,6 @@ export type Registration = {
export class ControllerModel {
private readonly _azdataApi: azdataExt.IExtension;
private _endpoints: azdataExt.DcEndpointListResult[] = [];
private _namespace: string = '';
private _registrations: Registration[] = [];
private _controllerConfig: azdataExt.DcConfigShowResult | undefined = undefined;
@@ -93,7 +92,7 @@ export class ControllerModel {
}
public async refresh(showErrors: boolean = true, promptReconnect: boolean = false): Promise<void> {
await this.azdataLogin(promptReconnect);
this._registrations = [];
const newRegistrations: Registration[] = [];
await Promise.all([
this._azdataApi.azdata.arc.dc.config.show().then(result => {
this._controllerConfig = result.result;
@@ -125,7 +124,7 @@ export class ControllerModel {
}),
Promise.all([
this._azdataApi.azdata.arc.postgres.server.list().then(result => {
this._registrations.push(...result.result.map(r => {
newRegistrations.push(...result.result.map(r => {
return {
instanceName: r.name,
state: r.state,
@@ -134,7 +133,7 @@ export class ControllerModel {
}));
}),
this._azdataApi.azdata.arc.sql.mi.list().then(result => {
this._registrations.push(...result.result.map(r => {
newRegistrations.push(...result.result.map(r => {
return {
instanceName: r.name,
state: r.state,
@@ -143,6 +142,7 @@ export class ControllerModel {
}));
})
]).then(() => {
this._registrations = newRegistrations;
this.registrationsLastUpdated = new Date();
this._onRegistrationsUpdated.fire(this._registrations);
})
@@ -157,10 +157,6 @@ export class ControllerModel {
return this._endpoints.find(e => e.name === name);
}
public get namespace(): string {
return this._namespace;
}
public get registrations(): Registration[] {
return this._registrations;
}
@@ -171,19 +167,10 @@ export class ControllerModel {
public getRegistration(type: ResourceType, name: string): Registration | undefined {
return this._registrations.find(r => {
return r.instanceType === type && parseInstanceName(r.instanceName) === name;
return r.instanceType === type && r.instanceName === name;
});
}
public async deleteRegistration(_type: ResourceType, _name: string) {
/* TODO chgagnon
if (r && !r.isDeleted && r.customObjectName) {
const r = this.getRegistration(type, name);
await this._registrationRouter.apiV1RegistrationNsNameIsDeletedDelete(this._namespace, r.customObjectName, true);
}
*/
}
/**
* property to for use a display label for this controller
*/

View File

@@ -3,13 +3,15 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ResourceInfo } from 'arc';
import { MiaaResourceInfo } from 'arc';
import * as azdata from 'azdata';
import * as azdataExt from 'azdata-ext';
import * as vscode from 'vscode';
import { Deferred } from '../common/promise';
import { UserCancelledError } from '../common/utils';
import { createCredentialId, parseIpAndPort, UserCancelledError } from '../common/utils';
import { credentialNamespace } from '../constants';
import * as loc from '../localizedConstants';
import { ConnectToSqlDialog } from '../ui/dialogs/connectSqlDialog';
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
import { ControllerModel, Registration } from './controllerModel';
import { ResourceModel } from './resourceModel';
@@ -35,8 +37,8 @@ export class MiaaModel extends ResourceModel {
private _refreshPromise: Deferred<void> | undefined = undefined;
constructor(private _controllerModel: ControllerModel, info: ResourceInfo, registration: Registration, private _treeDataProvider: AzureArcTreeDataProvider) {
super(info, registration);
constructor(private _controllerModel: ControllerModel, private _miaaInfo: MiaaResourceInfo, registration: Registration, private _treeDataProvider: AzureArcTreeDataProvider) {
super(_miaaInfo, registration);
this._azdataApi = <azdataExt.IExtension>vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
}
@@ -155,53 +157,15 @@ export class MiaaModel extends ResourceModel {
if (this._connectionProfile) {
return;
}
let connection: azdata.connection.ConnectionProfile | azdata.connection.Connection | undefined;
if (this.info.connectionId) {
try {
const connections = await azdata.connection.getConnections();
const existingConnection = connections.find(conn => conn.connectionId === this.info.connectionId);
if (existingConnection) {
const credentials = await azdata.connection.getCredentials(this.info.connectionId);
if (credentials) {
existingConnection.options['password'] = credentials.password;
connection = existingConnection;
} else {
// We need the password so prompt the user for it
const connectionProfile: azdata.IConnectionProfile = {
serverName: existingConnection.options['serverName'],
databaseName: existingConnection.options['databaseName'],
authenticationType: existingConnection.options['authenticationType'],
providerName: 'MSSQL',
connectionName: '',
userName: existingConnection.options['user'],
password: '',
savePassword: false,
groupFullName: undefined,
saveProfile: true,
id: '',
groupId: undefined,
options: existingConnection.options
};
connection = await azdata.connection.openConnectionDialog(['MSSQL'], connectionProfile);
}
}
} catch (err) {
// ignore - the connection may not necessarily exist anymore and in that case we'll just reprompt for a connection
}
}
if (!connection) {
// We need the password so prompt the user for it
const connectionProfile: azdata.IConnectionProfile = {
// TODO chgagnon fill in external IP and port
// serverName: (this.registration.externalIp && this.registration.externalPort) ? `${this.registration.externalIp},${this.registration.externalPort}` : '',
serverName: '',
const ipAndPort = parseIpAndPort(this.config?.status.externalEndpoint || '');
let connectionProfile: azdata.IConnectionProfile | undefined = {
serverName: `${ipAndPort.ip},${ipAndPort.port}`,
databaseName: '',
authenticationType: 'SqlLogin',
providerName: 'MSSQL',
connectionName: '',
userName: 'sa',
userName: this._miaaInfo.userName || '',
password: '',
savePassword: true,
groupFullName: undefined,
@@ -210,28 +174,41 @@ export class MiaaModel extends ResourceModel {
groupId: undefined,
options: {}
};
// Weren't able to load the existing connection so prompt user for new one
connection = await azdata.connection.openConnectionDialog(['MSSQL'], connectionProfile);
// If we have the ID stored then try to retrieve the password from previous connections
if (this.info.connectionId) {
try {
const credentialProvider = await azdata.credentials.getProvider(credentialNamespace);
const credentials = await credentialProvider.readCredential(createCredentialId(this._controllerModel.info.id, this.info.resourceType, this.info.name));
if (credentials.password) {
// Try to connect to verify credentials are still valid
connectionProfile.password = credentials.password;
// If we don't have a username for some reason then just continue on and we'll prompt for the username below
if (connectionProfile.userName) {
const result = await azdata.connection.connect(connectionProfile, false, false);
if (!result.connected) {
vscode.window.showErrorMessage(loc.connectToSqlFailed(connectionProfile.serverName, result.errorMessage));
const connectToSqlDialog = new ConnectToSqlDialog(this._controllerModel, this);
connectToSqlDialog.showDialog(connectionProfile);
connectionProfile = await connectToSqlDialog.waitForClose();
}
}
}
} catch (err) {
console.warn(`Unexpected error fetching password for MIAA instance ${err}`);
// ignore - something happened fetching the password so just reprompt
}
}
if (connection) {
const profile = {
// The option name might be different here based on where it came from
serverName: connection.options['serverName'] || connection.options['server'],
databaseName: connection.options['databaseName'] || connection.options['database'],
authenticationType: connection.options['authenticationType'],
providerName: 'MSSQL',
connectionName: '',
userName: connection.options['user'],
password: connection.options['password'],
savePassword: false,
groupFullName: undefined,
saveProfile: true,
id: connection.connectionId,
groupId: undefined,
options: connection.options
};
this.updateConnectionProfile(profile);
if (!connectionProfile?.userName || !connectionProfile?.password) {
// Need to prompt user for password since we don't have one stored
const connectToSqlDialog = new ConnectToSqlDialog(this._controllerModel, this);
connectToSqlDialog.showDialog(connectionProfile);
connectionProfile = await connectToSqlDialog.waitForClose();
}
if (connectionProfile) {
this.updateConnectionProfile(connectionProfile);
} else {
throw new UserCancelledError();
}
@@ -240,6 +217,7 @@ export class MiaaModel extends ResourceModel {
private async updateConnectionProfile(connectionProfile: azdata.IConnectionProfile): Promise<void> {
this._connectionProfile = connectionProfile;
this.info.connectionId = connectionProfile.id;
this._miaaInfo.userName = connectionProfile.userName;
await this._treeDataProvider.saveControllers();
}
}

View File

@@ -4,278 +4,83 @@
*--------------------------------------------------------------------------------------------*/
import { ResourceInfo } from 'arc';
import * as azdataExt from 'azdata-ext';
import * as vscode from 'vscode';
import * as loc from '../localizedConstants';
import { Registration } from './controllerModel';
import { ControllerModel, Registration } from './controllerModel';
import { ResourceModel } from './resourceModel';
export enum PodRole {
Monitor,
Router,
Shard
}
export interface V1Pod {
'apiVersion'?: string;
'kind'?: string;
'metadata'?: any; // V1ObjectMeta;
'spec'?: any; // V1PodSpec;
'status'?: V1PodStatus;
}
export interface V1PodStatus {
'conditions'?: any[]; // Array<V1PodCondition>;
'containerStatuses'?: Array<V1ContainerStatus>;
'ephemeralContainerStatuses'?: any[]; // Array<V1ContainerStatus>;
'hostIP'?: string;
'initContainerStatuses'?: any[]; // Array<V1ContainerStatus>;
'message'?: string;
'nominatedNodeName'?: string;
'phase'?: string;
'podIP'?: string;
'podIPs'?: any[]; // Array<V1PodIP>;
'qosClass'?: string;
'reason'?: string;
'startTime'?: Date | null;
}
export interface V1ContainerStatus {
'containerID'?: string;
'image'?: string;
'imageID'?: string;
'lastState'?: any; // V1ContainerState;
'name'?: string;
'ready'?: boolean;
'restartCount'?: number;
'started'?: boolean | null;
'state'?: any; // V1ContainerState;
}
export interface DuskyObjectModelsDatabaseService {
'apiVersion'?: string;
'kind'?: string;
'metadata'?: any; // V1ObjectMeta;
'spec'?: any; // DuskyObjectModelsDatabaseServiceSpec;
'status'?: any; // DuskyObjectModelsDatabaseServiceStatus;
'arc'?: any; // DuskyObjectModelsDatabaseServiceArcPayload;
}
export interface V1Status {
'apiVersion'?: string;
'code'?: number | null;
'details'?: any; // V1StatusDetails;
'kind'?: string;
'message'?: string;
'metadata'?: any; // V1ListMeta;
'reason'?: string;
'status'?: string;
'hasObject'?: boolean;
}
export interface DuskyObjectModelsDatabase {
'name'?: string;
'owner'?: string;
'sharded'?: boolean | null;
}
import { parseIpAndPort } from '../common/utils';
export class PostgresModel extends ResourceModel {
private _service?: DuskyObjectModelsDatabaseService;
private _pods?: V1Pod[];
private readonly _onServiceUpdated = new vscode.EventEmitter<DuskyObjectModelsDatabaseService>();
private readonly _onPodsUpdated = new vscode.EventEmitter<V1Pod[]>();
public onServiceUpdated = this._onServiceUpdated.event;
public onPodsUpdated = this._onPodsUpdated.event;
public serviceLastUpdated?: Date;
public podsLastUpdated?: Date;
private _config?: azdataExt.PostgresServerShowResult;
private readonly _azdataApi: azdataExt.IExtension;
constructor(info: ResourceInfo, registration: Registration) {
private readonly _onConfigUpdated = new vscode.EventEmitter<azdataExt.PostgresServerShowResult>();
public onConfigUpdated = this._onConfigUpdated.event;
public configLastUpdated?: Date;
constructor(private _controllerModel: ControllerModel, info: ResourceInfo, registration: Registration) {
super(info, registration);
this._azdataApi = <azdataExt.IExtension>vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
}
/** Returns the service's Kubernetes namespace */
public get namespace(): string | undefined {
return ''; // TODO chgagnon return this.info.namespace;
/** Returns the configuration of Postgres */
public get config(): azdataExt.PostgresServerShowResult | undefined {
return this._config;
}
/** Returns the service's name */
public get name(): string {
return this.info.name;
/** Returns the major version of Postgres */
public get engineVersion(): string | undefined {
const kind = this._config?.kind;
return kind
? kind.substring(kind.lastIndexOf('-') + 1)
: undefined;
}
/** Returns the service's fully qualified name in the format namespace.name */
public get fullName(): string {
return `${this.namespace}.${this.name}`;
/** Returns the IP address and port of Postgres */
public get endpoint(): { ip: string, port: string } | undefined {
return this._config?.status.externalEndpoint
? parseIpAndPort(this._config.status.externalEndpoint)
: undefined;
}
/** Returns the service's spec */
public get service(): DuskyObjectModelsDatabaseService | undefined {
return this._service;
/** Returns the scale configuration of Postgres e.g. '3 nodes, 1.5 vCores, 1Gi RAM, 2Gi storage per node' */
public get scaleConfiguration(): string | undefined {
if (!this._config) {
return undefined;
}
/** Returns the service's pods */
public get pods(): V1Pod[] | undefined {
return this._pods;
}
/** Refreshes the model */
public async refresh() {
await Promise.all([
/* TODO enable
this._databaseRouter.getDuskyDatabaseService(this.info.namespace || 'test', this.info.name).then(response => {
this._service = response.body;
this.serviceLastUpdated = new Date();
this._onServiceUpdated.fire(this._service);
}),
this._databaseRouter.getDuskyPods(this.info.namespace || 'test', this.info.name).then(response => {
this._pods = response.body;
this.podsLastUpdated = new Date();
this._onPodsUpdated.fire(this._pods!);
})
*/
]);
}
/**
* Updates the service
* @param func A function of modifications to apply to the service
*/
public async update(_func: (service: DuskyObjectModelsDatabaseService) => void): Promise<DuskyObjectModelsDatabaseService> {
return <any>undefined;
/*
// Get the latest spec of the service in case it has changed
const service = (await this._databaseRouter.getDuskyDatabaseService(this.info.namespace || 'test', this.info.name)).body;
service.status = undefined; // can't update the status
func(service);
return await this._databaseRouter.updateDuskyDatabaseService(this.namespace || 'test', this.name, service).then(r => {
this._service = r.body;
return this._service;
});
*/
}
/** Deletes the service */
public async delete(): Promise<V1Status> {
return <any>undefined;
// return (await this._databaseRouter.deleteDuskyDatabaseService(this.info.namespace || 'test', this.info.name)).body;
}
/** Creates a SQL database in the service */
public async createDatabase(_db: DuskyObjectModelsDatabase): Promise<DuskyObjectModelsDatabase> {
return <any>undefined;
// return (await this._databaseRouter.createDuskyDatabase(this.namespace || 'test', this.name, db)).body;
}
/**
* Returns the IP address and port of the service, preferring external IP over
* internal IP. If either field is not available it will be set to undefined.
*/
public get endpoint(): { ip?: string, port?: number } {
const externalIp = this._service?.status?.externalIP;
const internalIp = this._service?.status?.internalIP;
const externalPort = this._service?.status?.externalPort;
const internalPort = this._service?.status?.internalPort;
return externalIp ? { ip: externalIp, port: externalPort ?? undefined }
: internalIp ? { ip: internalIp, port: internalPort ?? undefined }
: { ip: undefined, port: undefined };
}
/** Returns the service's configuration e.g. '3 nodes, 1.5 vCores, 1GiB RAM, 2GiB storage per node' */
public get configuration(): string {
// TODO: Resource requests and limits can be configured per role. Figure out how
// to display that in the UI. For now, only show the default configuration.
const cpuLimit = this._service?.spec?.scheduling?._default?.resources?.limits?.['cpu'];
const ramLimit = this._service?.spec?.scheduling?._default?.resources?.limits?.['memory'];
const cpuRequest = this._service?.spec?.scheduling?._default?.resources?.requests?.['cpu'];
const ramRequest = this._service?.spec?.scheduling?._default?.resources?.requests?.['memory'];
const storage = this._service?.spec?.storage?.volumeSize;
const nodes = this.pods?.length;
const cpuLimit = this._config.spec.scheduling?.default?.resources?.limits?.cpu;
const ramLimit = this._config.spec.scheduling?.default?.resources?.limits?.memory;
const cpuRequest = this._config.spec.scheduling?.default?.resources?.requests?.cpu;
const ramRequest = this._config.spec.scheduling?.default?.resources?.requests?.memory;
const storage = this._config.spec.storage?.data?.size;
const nodes = (this._config.spec.scale?.shards ?? 0) + 1; // An extra node for the coordinator
let configuration: string[] = [];
if (nodes) {
configuration.push(`${nodes} ${nodes > 1 ? loc.nodes : loc.node}`);
}
// Prefer limits if they're provided, otherwise use requests if they're provided
if (cpuLimit || cpuRequest) {
configuration.push(`${this.formatCores(cpuLimit ?? cpuRequest!)} ${loc.vCores}`);
configuration.push(`${cpuLimit ?? cpuRequest!} ${loc.vCores}`);
}
if (ramLimit || ramRequest) {
configuration.push(`${this.formatMemory(ramLimit ?? ramRequest!)} ${loc.ram}`);
configuration.push(`${ramLimit ?? ramRequest!} ${loc.ram}`);
}
if (storage) {
configuration.push(`${this.formatMemory(storage)} ${loc.storagePerNode}`);
configuration.push(`${storage} ${loc.storagePerNode}`);
}
return configuration.join(', ');
}
/** Given a V1Pod, returns its PodRole or undefined if the role isn't known */
public static getPodRole(pod: V1Pod): PodRole | undefined {
const name = pod.metadata?.name;
const role = name?.substring(name.lastIndexOf('-'))[1];
switch (role) {
case 'm': return PodRole.Monitor;
case 'r': return PodRole.Router;
case 's': return PodRole.Shard;
default: return undefined;
}
}
/** Given a PodRole, returns its localized name */
public static getPodRoleName(role?: PodRole): string {
switch (role) {
case PodRole.Monitor: return loc.monitor;
case PodRole.Router: return loc.coordinator;
case PodRole.Shard: return loc.worker;
default: return '';
}
}
/** Given a V1Pod returns its status */
public static getPodStatus(pod: V1Pod): string {
const phase = pod.status?.phase;
if (phase !== 'Running') {
return phase ?? '';
}
// Pods can be in the running phase while some
// containers are crashing, so check those too.
for (let c of pod.status?.containerStatuses?.filter(c => !c.ready) ?? []) {
const wReason = c.state?.waiting?.reason;
const tReason = c.state?.terminated?.reason;
if (wReason) { return wReason; }
if (tReason) { return tReason; }
}
return loc.running;
}
/**
* Converts millicores to cores (600m -> 0.6 cores)
* https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-cpu
* @param cores The millicores to format e.g. 600m
*/
private formatCores(cores: string): number {
return cores?.endsWith('m') ? +cores.slice(0, -1) / 1000 : +cores;
}
/**
* Formats the memory to end with 'B' e.g:
* 1 -> 1B
* 1K -> 1KB, 1Ki -> 1KiB
* 1M -> 1MB, 1Mi -> 1MiB
* 1G -> 1GB, 1Gi -> 1GiB
* 1T -> 1TB, 1Ti -> 1TiB
* https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-memory
* @param memory The amount + unit of memory to format e.g. 1K
*/
private formatMemory(memory: string): string {
return memory && !memory.endsWith('B') ? `${memory}B` : memory;
/** Refreshes the model */
public async refresh() {
await this._controllerModel.azdataLogin();
this._config = (await this._azdataApi.azdata.arc.postgres.server.show(this.info.name)).result;
this.configLastUpdated = new Date();
this._onConfigUpdated.fire(this._config);
}
}

View File

@@ -41,7 +41,9 @@ export class ArcControllersOptionsSourceProvider implements rd.IOptionsSourcePro
}
getVariableValue(variableName: string, controllerLabel: string): Promise<string> {
return this._cacheManager.getCacheEntry(JSON.stringify([variableName, controllerLabel]), this.retrieveVariable);
// capture 'this' in an arrow function object
const retrieveVariable = (key: string) => this.retrieveVariable(key);
return this._cacheManager.getCacheEntry(JSON.stringify([variableName, controllerLabel]), retrieveVariable);
}
private async getPassword(controller: arc.DataController): Promise<string> {

View File

@@ -7,7 +7,7 @@ import { ResourceType } from 'arc';
import 'mocha';
import * as should from 'should';
import * as vscode from 'vscode';
import { getAzurecoreApi, getConnectionModeDisplayText, getDatabaseStateDisplayText, getErrorMessage, getResourceTypeIcon, parseEndpoint, parseInstanceName, parseIpAndPort, promptAndConfirmPassword, promptForResourceDeletion, resourceTypeToDisplayName } from '../../common/utils';
import { getAzurecoreApi, getConnectionModeDisplayText, getDatabaseStateDisplayText, getErrorMessage, getResourceTypeIcon, parseEndpoint, parseIpAndPort, promptAndConfirmPassword, promptForInstanceDeletion, resourceTypeToDisplayName } from '../../common/utils';
import { ConnectionMode as ConnectionMode, IconPathHelper } from '../../constants';
import * as loc from '../../localizedConstants';
import { MockInputBox } from '../stubs';
@@ -47,24 +47,6 @@ describe('parseEndpoint Method Tests', function (): void {
});
});
describe('parseInstanceName Method Tests', () => {
it('Should parse valid instanceName with namespace correctly', function (): void {
should(parseInstanceName('mynamespace_myinstance')).equal('myinstance');
});
it('Should parse valid instanceName without namespace correctly', function (): void {
should(parseInstanceName('myinstance')).equal('myinstance');
});
it('Should return empty string when undefined value passed in', function (): void {
should(parseInstanceName(undefined)).equal('');
});
it('Should return empty string when empty string value passed in', function (): void {
should(parseInstanceName('')).equal('');
});
});
describe('getAzurecoreApi Method Tests', function () {
it('Should get azurecore API correctly', function (): void {
should(getAzurecoreApi()).not.be.undefined();
@@ -140,7 +122,7 @@ describe('promptForResourceDeletion Method Tests', function (): void {
});
it('Resolves as true when value entered is correct', function (done): void {
promptForResourceDeletion('myname').then((value: boolean) => {
promptForInstanceDeletion('myname').then((value: boolean) => {
value ? done() : done(new Error('Expected return value to be true'));
});
mockInputBox.value = 'myname';
@@ -148,14 +130,14 @@ describe('promptForResourceDeletion Method Tests', function (): void {
});
it('Resolves as false when input box is closed early', function (done): void {
promptForResourceDeletion('myname').then((value: boolean) => {
promptForInstanceDeletion('myname').then((value: boolean) => {
!value ? done() : done(new Error('Expected return value to be false'));
});
mockInputBox.hide();
});
it('Validation message is set when value entered is incorrect', async function (): Promise<void> {
promptForResourceDeletion('myname');
promptForInstanceDeletion('myname');
mockInputBox.value = 'wrong value';
await mockInputBox.triggerAccept();
should(mockInputBox.validationMessage).not.be.equal('', 'Validation message should not be empty after incorrect value entered');
@@ -260,22 +242,6 @@ describe('getErrorMessage Method Tests', function () {
});
});
describe('parseInstanceName Method Tests', function () {
it('2 part name', function (): void {
const name = 'MyName';
should(parseInstanceName(`MyNamespace_${name}`)).equal(name);
});
it('1 part name', function (): void {
const name = 'MyName';
should(parseInstanceName(name)).equal(name);
});
it('Invalid name', function (): void {
should(() => parseInstanceName('Some_Invalid_Name')).throwError();
});
});
describe('parseIpAndPort', function (): void {
it('Valid address', function (): void {
const ip = '127.0.0.1';

View File

@@ -0,0 +1,77 @@
/*---------------------------------------------------------------------------------------------
* 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 {
public postgresInstances: azdataExt.PostgresServerListResult[] = [];
public miaaInstances: azdataExt.SqlMiListResult[] = [];
//
// API Implementation
//
public get arc() {
const self = this;
return {
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: {
delete(_name: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
async list(): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerListResult[]>> { return <any>{ result: self.postgresInstances }; },
show(_name: string): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerShowResult>> { throw new Error('Method not implemented.'); },
edit(
_name: string,
_args: {
adminPassword?: boolean,
coresLimit?: string,
coresRequest?: string,
engineSettings?: string,
extensions?: string,
memoryLimit?: string,
memoryRequest?: string,
noWait?: boolean,
port?: number,
replaceEngineSettings?: boolean,
workers?: number
},
_additionalEnvVars?: { [key: string]: string }): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); }
}
},
sql: {
mi: {
delete(_name: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
async list(): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiListResult[]>> { return <any>{ result: self.miaaInstances }; },
show(_name: string): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiShowResult>> { throw new Error('Method not implemented.'); }
}
}
};
}
getPath(): Promise<string> {
throw new Error('Method not implemented.');
}
login(_endpoint: string, _username: string, _password: string): Promise<azdataExt.AzdataOutput<any>> {
return <any>undefined;
}
version(): Promise<azdataExt.AzdataOutput<string>> {
throw new Error('Method not implemented.');
}
getSemVersion(): any {
throw new Error('Method not implemented.');
}
}

View File

@@ -3,16 +3,21 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ControllerInfo } from 'arc';
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 { 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';
describe('AzureArcTreeDataProvider tests', function (): void {
let treeDataProvider: AzureArcTreeDataProvider;
@@ -84,6 +89,27 @@ describe('AzureArcTreeDataProvider tests', function (): void {
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();
fakeAzdataApi.postgresInstances = [{ name: 'pg1', state: '', workers: 0 }];
fakeAzdataApi.miaaInstances = [{ name: 'miaa1', state: '', replicas: '', serverEndpoint: '' }];
mockArcApi.setup(x => x.azdata).returns(() => fakeAzdataApi);
sinon.stub(vscode.extensions, 'getExtension').returns(mockArcExtension.object);
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }, 'mypassword');
await treeDataProvider.addOrUpdateController(controllerModel, '');
const controllerNode = treeDataProvider.getControllerNode(controllerModel);
const children = await treeDataProvider.getChildren(controllerNode);
should(children.filter(c => c.label === fakeAzdataApi.postgresInstances[0].name).length).equal(1, 'Should have a Postgres child');
should(children.filter(c => c.label === fakeAzdataApi.miaaInstances[0].name).length).equal(1, 'Should have a MIAA child');
should(children.length).equal(2, 'Should have excatly 2 children');
});
});
describe('removeController', function (): void {
@@ -104,4 +130,31 @@ describe('AzureArcTreeDataProvider tests', function (): void {
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, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
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, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
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, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
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

@@ -3,7 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module 'arc' {
import * as vscode from 'vscode';
/**
* Covers defining what the arc extension exports to other extensions
@@ -20,6 +19,10 @@ declare module 'arc' {
sqlManagedInstances = 'sqlManagedInstances'
}
export type MiaaResourceInfo = ResourceInfo & {
userName?: string
};
export type ResourceInfo = {
name: string,
resourceType: ResourceType | string,

View File

@@ -7,8 +7,8 @@ import { ResourceType } from 'arc';
import * as azdata from 'azdata';
import * as azurecore from 'azurecore';
import * as vscode from 'vscode';
import { getConnectionModeDisplayText, getResourceTypeIcon, parseInstanceName, resourceTypeToDisplayName } from '../../../common/utils';
import { cssStyles, Endpoints, IconPathHelper, iconSize } from '../../../constants';
import { getConnectionModeDisplayText, getResourceTypeIcon, resourceTypeToDisplayName } from '../../../common/utils';
import { cssStyles, Endpoints, IconPathHelper, controllerTroubleshootDocsUrl, iconSize } from '../../../constants';
import * as loc from '../../../localizedConstants';
import { ControllerModel } from '../../../models/controllerModel';
import { DashboardPage } from '../../components/dashboardPage';
@@ -93,7 +93,7 @@ export class ControllerDashboardOverviewPage extends DashboardPage {
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: cssStyles.tableRow
}, {
displayName: loc.compute,
displayName: loc.state,
valueType: azdata.DeclarativeDataType.string,
width: '34%',
isReadOnly: true,
@@ -178,18 +178,30 @@ export class ControllerDashboardOverviewPage extends DashboardPage {
this._openInAzurePortalButton.onDidClick(async () => {
const config = this._controllerModel.controllerConfig;
if (config) {
vscode.env.openExternal(vscode.Uri.parse(
await vscode.env.openExternal(vscode.Uri.parse(
`https://portal.azure.com/#resource/subscriptions/${config.spec.settings.azure.subscription}/resourceGroups/${config.spec.settings.azure.resourceGroup}/providers/Microsoft.AzureData/${ResourceType.dataControllers}/${config.metadata.name}`));
} else {
vscode.window.showErrorMessage(loc.couldNotFindControllerRegistration);
}
}));
const troubleshootButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
label: loc.troubleshoot,
iconPath: IconPathHelper.wrench
}).component();
this.disposables.push(
troubleshootButton.onDidClick(async () => {
await vscode.env.openExternal(vscode.Uri.parse(controllerTroubleshootDocsUrl));
})
);
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems(
[
{ component: newInstance },
{ component: refreshButton, toolbarSeparatorAfter: true },
{ component: this._openInAzurePortalButton }
{ component: this._openInAzurePortalButton, toolbarSeparatorAfter: true },
{ component: troubleshootButton }
]
).component();
}
@@ -219,26 +231,18 @@ export class ControllerDashboardOverviewPage extends DashboardPage {
iconHeight: iconSize,
iconWidth: iconSize
}).component();
let nameComponent: azdata.Component;
if (r.instanceType === ResourceType.postgresInstances) {
nameComponent = this.modelView.modelBuilder.text()
.withProperties<azdata.TextComponentProperties>({
value: r.instanceName || '',
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
} else {
nameComponent = this.modelView.modelBuilder.hyperlink()
const nameComponent = this.modelView.modelBuilder.hyperlink()
.withProperties<azdata.HyperlinkComponentProperties>({
label: r.instanceName || '',
url: ''
}).component();
(<azdata.HyperlinkComponent>nameComponent).onDidClick(async () => {
await this._controllerModel.treeDataProvider.openResourceDashboard(this._controllerModel, r.instanceType || '', parseInstanceName(r.instanceName));
});
}
// TODO chgagnon
return [imageComponent, nameComponent, resourceTypeToDisplayName(r.instanceType), '-'/* loc.numVCores(r.vCores) */];
this.disposables.push(nameComponent.onDidClick(async () => {
await this._controllerModel.treeDataProvider.openResourceDashboard(this._controllerModel, r.instanceType || '', r.instanceName);
}));
return [imageComponent, nameComponent, resourceTypeToDisplayName(r.instanceType), r.state];
});
this._arcResourcesLoadingComponent.loading = !this._controllerModel.registrationsLastUpdated;
}

View File

@@ -7,8 +7,8 @@ import * as azdata from 'azdata';
import * as azdataExt from 'azdata-ext';
import * as azurecore from 'azurecore';
import * as vscode from 'vscode';
import { getDatabaseStateDisplayText, promptForResourceDeletion } from '../../../common/utils';
import { cssStyles, Endpoints, IconPathHelper } from '../../../constants';
import { getDatabaseStateDisplayText, promptForInstanceDeletion } from '../../../common/utils';
import { cssStyles, IconPathHelper, miaaTroubleshootDocsUrl } from '../../../constants';
import * as loc from '../../../localizedConstants';
import { ControllerModel } from '../../../models/controllerModel';
import { MiaaModel } from '../../../models/miaaModel';
@@ -198,13 +198,22 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
deleteButton.onDidClick(async () => {
deleteButton.enabled = false;
try {
if (await promptForResourceDeletion(this._miaaModel.info.name)) {
await this._azdataApi.azdata.arc.sql.mi.delete(this._miaaModel.info.name);
if (await promptForInstanceDeletion(this._miaaModel.info.name)) {
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: loc.deletingInstance(this._miaaModel.info.name),
cancellable: false
},
(_progress, _token) => {
return this._azdataApi.azdata.arc.sql.mi.delete(this._miaaModel.info.name);
}
);
await this._controllerModel.refreshTreeNode();
vscode.window.showInformationMessage(loc.resourceDeleted(this._miaaModel.info.name));
vscode.window.showInformationMessage(loc.instanceDeleted(this._miaaModel.info.name));
}
} catch (error) {
vscode.window.showErrorMessage(loc.resourceDeletionFailed(this._miaaModel.info.name, error));
vscode.window.showErrorMessage(loc.instanceDeletionFailed(this._miaaModel.info.name, error));
} finally {
deleteButton.enabled = true;
}
@@ -248,6 +257,17 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
}
}));
const troubleshootButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
label: loc.troubleshoot,
iconPath: IconPathHelper.wrench
}).component();
this.disposables.push(
troubleshootButton.onDidClick(async () => {
await vscode.env.openExternal(vscode.Uri.parse(miaaTroubleshootDocsUrl));
})
);
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems(
[
{ component: deleteButton },
@@ -332,19 +352,13 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
}
private refreshDashboardLinks(): void {
const kibanaEndpoint = this._controllerModel.getEndpoint(Endpoints.logsui);
if (kibanaEndpoint && this._miaaModel.config) {
const kibanaQuery = `kubernetes_namespace:"${this._miaaModel.config.metadata.namespace}" and custom_resource_name :"${this._miaaModel.config.metadata.name}"`;
const kibanaUrl = `${kibanaEndpoint.endpoint}/app/kibana#/discover?_a=(query:(language:kuery,query:'${kibanaQuery}'))`;
if (this._miaaModel.config) {
const kibanaUrl = this._miaaModel.config.status.logSearchDashboard ?? '';
this._kibanaLink.label = kibanaUrl;
this._kibanaLink.url = kibanaUrl;
this._kibanaLoading!.loading = false;
}
const grafanaEndpoint = this._controllerModel.getEndpoint(Endpoints.metricsui);
if (grafanaEndpoint && this._miaaModel.config) {
const grafanaQuery = `var-hostname=${this._miaaModel.info.name}-0`;
const grafanaUrl = grafanaEndpoint ? `${grafanaEndpoint.endpoint}/d/40q72HnGk/sql-managed-instance-metrics?${grafanaQuery}` : '';
const grafanaUrl = this._miaaModel.config.status.metricsDashboard ?? '';
this._grafanaLink.label = grafanaUrl;
this._grafanaLink.url = grafanaUrl;
this._grafanaLoading!.loading = false;

View File

@@ -1,31 +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 azdata from 'azdata';
import * as loc from '../../../localizedConstants';
import { IconPathHelper } from '../../../constants';
import { DashboardPage } from '../../components/dashboardPage';
export class PostgresBackupPage extends DashboardPage {
protected get title(): string {
return loc.backup;
}
protected get id(): string {
return 'postgres-backup';
}
protected get icon(): { dark: string; light: string; } {
return IconPathHelper.backup;
}
protected get container(): azdata.Component {
return this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: loc.backup }).component();
}
protected get toolbarContainer(): azdata.ToolbarContainer {
return this.modelView.modelBuilder.toolbarContainer().component();
}
}

View File

@@ -1,31 +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 azdata from 'azdata';
import * as loc from '../../../localizedConstants';
import { IconPathHelper } from '../../../constants';
import { DashboardPage } from '../../components/dashboardPage';
export class PostgresComputeStoragePage extends DashboardPage {
protected get title(): string {
return loc.computeAndStorage;
}
protected get id(): string {
return 'postgres-compute-storage';
}
protected get icon(): { dark: string; light: string; } {
return IconPathHelper.computeStorage;
}
protected get container(): azdata.Component {
return this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: loc.computeAndStorage }).component();
}
protected get toolbarContainer(): azdata.ToolbarContainer {
return this.modelView.modelBuilder.toolbarContainer().component();
}
}

View File

@@ -3,7 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as loc from '../../../localizedConstants';
import { IconPathHelper, cssStyles } from '../../../constants';
@@ -12,13 +11,12 @@ import { DashboardPage } from '../../components/dashboardPage';
import { PostgresModel } from '../../../models/postgresModel';
export class PostgresConnectionStringsPage extends DashboardPage {
private loading?: azdata.LoadingComponent;
private keyValueContainer?: KeyValueContainer;
constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) {
super(modelView);
this.disposables.push(this._postgresModel.onServiceUpdated(
this.disposables.push(this._postgresModel.onConfigUpdated(
() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())));
}
@@ -61,44 +59,20 @@ export class PostgresConnectionStringsPage extends DashboardPage {
this.keyValueContainer = new KeyValueContainer(this.modelView.modelBuilder, this.getConnectionStrings());
this.disposables.push(this.keyValueContainer);
this.loading = this.modelView.modelBuilder.loadingComponent()
.withItem(this.keyValueContainer.container)
.withProperties<azdata.LoadingComponentProperties>({
loading: !this._postgresModel.serviceLastUpdated
}).component();
content.addItem(this.loading);
content.addItem(this.keyValueContainer.container);
this.initialized = true;
return root;
}
protected get toolbarContainer(): azdata.ToolbarContainer {
const refreshButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
label: loc.refresh,
iconPath: IconPathHelper.refresh
}).component();
this.disposables.push(
refreshButton.onDidClick(async () => {
refreshButton.enabled = false;
try {
this.loading!.loading = true;
await this._postgresModel.refresh();
} catch (error) {
vscode.window.showErrorMessage(loc.refreshFailed(error));
} finally {
refreshButton.enabled = true;
}
}));
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([
{ component: refreshButton }
]).component();
return this.modelView.modelBuilder.toolbarContainer().component();
}
private getConnectionStrings(): KeyValue[] {
const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint;
const endpoint = this._postgresModel.endpoint;
if (!endpoint) {
return [];
}
return [
new InputKeyValue(this.modelView.modelBuilder, 'ADO.NET', `Server=${endpoint.ip};Database=postgres;Port=${endpoint.port};User Id=postgres;Password={your_password_here};Ssl Mode=Require;`),
@@ -115,6 +89,5 @@ export class PostgresConnectionStringsPage extends DashboardPage {
private handleServiceUpdated() {
this.keyValueContainer?.refresh(this.getConnectionStrings());
this.loading!.loading = false;
}
}

View File

@@ -10,15 +10,13 @@ import { ControllerModel } from '../../../models/controllerModel';
import { PostgresModel } from '../../../models/postgresModel';
import { PostgresOverviewPage } from './postgresOverviewPage';
import { PostgresConnectionStringsPage } from './postgresConnectionStringsPage';
import { PostgresPropertiesPage } from './postgresPropertiesPage';
import { Dashboard } from '../../components/dashboard';
import { PostgresDiagnoseAndSolveProblemsPage } from './postgresDiagnoseAndSolveProblemsPage';
import { PostgresSupportRequestPage } from './postgresSupportRequestPage';
import { PostgresResourceHealthPage } from './postgresResourceHealthPage';
export class PostgresDashboard extends Dashboard {
constructor(private _context: vscode.ExtensionContext, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
super(loc.postgresDashboard(_postgresModel.name), 'ArcPgDashboard');
super(loc.postgresDashboard(_postgresModel.info.name), 'ArcPgDashboard');
}
public async showDashboard(): Promise<void> {
@@ -32,24 +30,22 @@ export class PostgresDashboard extends Dashboard {
protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> {
const overviewPage = new PostgresOverviewPage(modelView, this._controllerModel, this._postgresModel);
const connectionStringsPage = new PostgresConnectionStringsPage(modelView, this._postgresModel);
const propertiesPage = new PostgresPropertiesPage(modelView, this._controllerModel, this._postgresModel);
const resourceHealthPage = new PostgresResourceHealthPage(modelView, this._postgresModel);
// TODO: Removed properties page while investigating bug where refreshed values don't appear in UI
// const propertiesPage = new PostgresPropertiesPage(modelView, this._controllerModel, this._postgresModel);
const diagnoseAndSolveProblemsPage = new PostgresDiagnoseAndSolveProblemsPage(modelView, this._context, this._postgresModel);
const supportRequestPage = new PostgresSupportRequestPage(modelView);
const supportRequestPage = new PostgresSupportRequestPage(modelView, this._controllerModel, this._postgresModel);
return [
overviewPage.tab,
{
title: loc.settings,
tabs: [
connectionStringsPage.tab,
propertiesPage.tab
connectionStringsPage.tab
]
},
{
title: loc.supportAndTroubleshooting,
tabs: [
resourceHealthPage.tab,
diagnoseAndSolveProblemsPage.tab,
supportRequestPage.tab
]

View File

@@ -50,8 +50,9 @@ export class PostgresDiagnoseAndSolveProblemsPage extends DashboardPage {
this.disposables.push(
troubleshootButton.onDidClick(() => {
process.env['POSTGRES_SERVER_NAMESPACE'] = this._postgresModel.namespace;
process.env['POSTGRES_SERVER_NAME'] = this._postgresModel.name;
process.env['POSTGRES_SERVER_NAMESPACE'] = this._postgresModel.config?.metadata.namespace;
process.env['POSTGRES_SERVER_NAME'] = this._postgresModel.info.name;
process.env['POSTGRES_SERVER_VERSION'] = this._postgresModel.engineVersion;
vscode.commands.executeCommand('bookTreeView.openBook', this._context.asAbsolutePath('notebooks/arcDataServices'), true, 'postgres/tsg100-troubleshoot-postgres');
}));

View File

@@ -1,31 +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 azdata from 'azdata';
import * as loc from '../../../localizedConstants';
import { IconPathHelper } from '../../../constants';
import { DashboardPage } from '../../components/dashboardPage';
export class PostgresNetworkingPage extends DashboardPage {
protected get title(): string {
return loc.networking;
}
protected get id(): string {
return 'postgres-networking';
}
protected get icon(): { dark: string; light: string; } {
return IconPathHelper.networking;
}
protected get container(): azdata.Component {
return this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: loc.networking }).component();
}
protected get toolbarContainer(): azdata.ToolbarContainer {
return this.modelView.modelBuilder.toolbarContainer().component();
}
}

View File

@@ -5,33 +5,34 @@
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as azdataExt from 'azdata-ext';
import * as loc from '../../../localizedConstants';
import { IconPathHelper, cssStyles, Endpoints } from '../../../constants';
import { IconPathHelper, cssStyles } from '../../../constants';
import { DashboardPage } from '../../components/dashboardPage';
import { ControllerModel } from '../../../models/controllerModel';
import { PostgresModel } from '../../../models/postgresModel';
import { promptAndConfirmPassword } from '../../../common/utils';
import { promptAndConfirmPassword, promptForInstanceDeletion } from '../../../common/utils';
import { ResourceType } from 'arc';
export class PostgresOverviewPage extends DashboardPage {
private propertiesLoading?: azdata.LoadingComponent;
private kibanaLoading?: azdata.LoadingComponent;
private grafanaLoading?: azdata.LoadingComponent;
private nodesTableLoading?: azdata.LoadingComponent;
private propertiesLoading!: azdata.LoadingComponent;
private kibanaLoading!: azdata.LoadingComponent;
private grafanaLoading!: azdata.LoadingComponent;
private properties?: azdata.PropertiesContainerComponent;
private kibanaLink?: azdata.HyperlinkComponent;
private grafanaLink?: azdata.HyperlinkComponent;
private nodesTable?: azdata.DeclarativeTableComponent;
private properties!: azdata.PropertiesContainerComponent;
private kibanaLink!: azdata.HyperlinkComponent;
private grafanaLink!: azdata.HyperlinkComponent;
private readonly _azdataApi: azdataExt.IExtension;
constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
super(modelView);
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
this.disposables.push(
this._controllerModel.onEndpointsUpdated(() => this.eventuallyRunOnInitialized(() => this.handleEndpointsUpdated())),
this._controllerModel.onRegistrationsUpdated(() => this.eventuallyRunOnInitialized(() => this.handleRegistrationsUpdated())),
this._postgresModel.onServiceUpdated(() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())),
this._postgresModel.onPodsUpdated(() => this.eventuallyRunOnInitialized(() => this.handlePodsUpdated())));
this._postgresModel.onConfigUpdated(() => this.eventuallyRunOnInitialized(() => this.handleConfigUpdated())));
}
protected get title(): string {
@@ -60,7 +61,7 @@ export class PostgresOverviewPage extends DashboardPage {
this.propertiesLoading = this.modelView.modelBuilder.loadingComponent()
.withItem(this.properties)
.withProperties<azdata.LoadingComponentProperties>({
loading: !this._controllerModel.registrationsLastUpdated && !this._postgresModel.serviceLastUpdated && !this._postgresModel.podsLastUpdated
loading: !this._controllerModel.registrationsLastUpdated && !this._postgresModel.configLastUpdated
}).component();
content.addItem(this.propertiesLoading, { CSSStyles: cssStyles.text });
@@ -72,29 +73,26 @@ export class PostgresOverviewPage extends DashboardPage {
CSSStyles: titleCSS
}).component());
this.kibanaLink = this.modelView.modelBuilder.hyperlink()
.withProperties<azdata.HyperlinkComponentProperties>({
label: this.getKibanaLink(),
url: this.getKibanaLink()
}).component();
this.kibanaLink = this.modelView.modelBuilder.hyperlink().component();
this.grafanaLink = this.modelView.modelBuilder.hyperlink()
.withProperties<azdata.HyperlinkComponentProperties>({
label: this.getGrafanaLink(),
url: this.getGrafanaLink()
}).component();
this.grafanaLink = this.modelView.modelBuilder.hyperlink().component();
this.kibanaLoading = this.modelView.modelBuilder.loadingComponent()
.withItem(this.kibanaLink)
.withProperties<azdata.LoadingComponentProperties>({
loading: !this._controllerModel.endpointsLastUpdated
}).component();
.withProperties<azdata.LoadingComponentProperties>(
{ loading: !this._postgresModel?.configLastUpdated }
)
.component();
this.grafanaLoading = this.modelView.modelBuilder.loadingComponent()
.withItem(this.grafanaLink)
.withProperties<azdata.LoadingComponentProperties>({
loading: !this._controllerModel.endpointsLastUpdated
}).component();
.withProperties<azdata.LoadingComponentProperties>(
{ loading: !this._postgresModel?.configLastUpdated }
)
.component();
this.refreshDashboardLinks();
this.kibanaLoading.component = this.kibanaLink;
this.grafanaLoading.component = this.grafanaLink;
const endpointsTable = this.modelView.modelBuilder.declarativeTable().withProperties<azdata.DeclarativeTableProperties>({
width: '100%',
@@ -134,60 +132,8 @@ export class PostgresOverviewPage extends DashboardPage {
[loc.kibanaDashboard, this.kibanaLoading, loc.kibanaDashboardDescription],
[loc.grafanaDashboard, this.grafanaLoading, loc.grafanaDashboardDescription]]
}).component();
content.addItem(endpointsTable);
// Server group nodes
content.addItem(this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: loc.serverGroupNodes,
CSSStyles: titleCSS
}).component());
this.nodesTable = this.modelView.modelBuilder.declarativeTable().withProperties<azdata.DeclarativeTableProperties>({
width: '100%',
columns: [
{
displayName: loc.name,
valueType: azdata.DeclarativeDataType.string,
isReadOnly: true,
width: '30%',
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: cssStyles.tableRow
},
{
displayName: loc.type,
valueType: azdata.DeclarativeDataType.string,
isReadOnly: true,
width: '15%',
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: cssStyles.tableRow
},
{
displayName: loc.status,
valueType: azdata.DeclarativeDataType.string,
isReadOnly: true,
width: '20%',
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: cssStyles.tableRow
},
{
displayName: loc.fullyQualifiedDomain,
valueType: azdata.DeclarativeDataType.string,
isReadOnly: true,
width: '35%',
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: cssStyles.tableRow
}
],
data: this.getNodes()
}).component();
this.nodesTableLoading = this.modelView.modelBuilder.loadingComponent()
.withItem(this.nodesTable)
.withProperties<azdata.LoadingComponentProperties>({
loading: !this._postgresModel.serviceLastUpdated && !this._postgresModel.podsLastUpdated
}).component();
content.addItem(this.nodesTableLoading, { CSSStyles: { 'margin-bottom': '20px' } });
this.initialized = true;
return root;
}
@@ -205,11 +151,13 @@ export class PostgresOverviewPage extends DashboardPage {
try {
const password = await promptAndConfirmPassword(input => !input ? loc.enterANonEmptyPassword : '');
if (password) {
await this._postgresModel.update(s => {
// TODO chgagnon
// s.arc = s.arc ?? new DuskyObjectModelsDatabaseServiceArcPayload();
s.arc.servicePassword = password;
});
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name,
{
adminPassword: true,
noWait: true
},
{ 'AZDATA_PASSWORD': password });
vscode.window.showInformationMessage(loc.passwordReset);
}
} catch (error) {
@@ -229,15 +177,22 @@ export class PostgresOverviewPage extends DashboardPage {
deleteButton.onDidClick(async () => {
deleteButton.enabled = false;
try {
/*
if (await promptForResourceDeletion(this._postgresModel.namespace, this._postgresModel.name)) {
await this._postgresModel.delete();
await this._controllerModel.deleteRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name);
vscode.window.showInformationMessage(loc.resourceDeleted(this._postgresModel.fullName));
if (await promptForInstanceDeletion(this._postgresModel.info.name)) {
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: loc.deletingInstance(this._postgresModel.info.name),
cancellable: false
},
(_progress, _token) => {
return this._azdataApi.azdata.arc.postgres.server.delete(this._postgresModel.info.name);
}
);
await this._controllerModel.refreshTreeNode();
vscode.window.showInformationMessage(loc.instanceDeleted(this._postgresModel.info.name));
}
*/
} catch (error) {
vscode.window.showErrorMessage(loc.resourceDeletionFailed(this._postgresModel.fullName, error));
vscode.window.showErrorMessage(loc.instanceDeletionFailed(this._postgresModel.info.name, error));
} finally {
deleteButton.enabled = true;
}
@@ -256,7 +211,6 @@ export class PostgresOverviewPage extends DashboardPage {
this.propertiesLoading!.loading = true;
this.kibanaLoading!.loading = true;
this.grafanaLoading!.loading = true;
this.nodesTableLoading!.loading = true;
await Promise.all([
this._postgresModel.refresh(),
@@ -278,15 +232,13 @@ export class PostgresOverviewPage extends DashboardPage {
this.disposables.push(
openInAzurePortalButton.onDidClick(async () => {
/*
const r = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name);
if (!r) {
vscode.window.showErrorMessage(loc.couldNotFindAzureResource(this._postgresModel.fullName));
} else {
const azure = this._controllerModel.controllerConfig?.spec.settings.azure;
if (azure) {
vscode.env.openExternal(vscode.Uri.parse(
`https://portal.azure.com/#resource/subscriptions/${r.subscriptionId}/resourceGroups/${r.resourceGroupName}/providers/Microsoft.AzureData/${ResourceType.postgresInstances}/${r.instanceName}`));
`https://portal.azure.com/#resource/subscriptions/${azure.subscription}/resourceGroups/${azure.resourceGroup}/providers/Microsoft.AzureData/${ResourceType.postgresInstances}/${this._postgresModel.info.name}`));
} else {
vscode.window.showErrorMessage(loc.couldNotFindControllerRegistration);
}
*/
}));
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([
@@ -298,63 +250,35 @@ export class PostgresOverviewPage extends DashboardPage {
}
private getProperties(): azdata.PropertiesContainerItem[] {
/*
const registration = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name);
const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint;
const status = this._postgresModel.config?.status;
const azure = this._controllerModel.controllerConfig?.spec.settings.azure;
return [
{ displayName: loc.name, value: this._postgresModel.name },
{ displayName: loc.coordinatorEndpoint, value: `postgresql://postgres@${endpoint.ip}:${endpoint.port}` },
{ displayName: loc.status, value: this._postgresModel.service?.status?.state ?? '' },
{ displayName: loc.resourceGroup, value: azure?.resourceGroup || '-' },
{ displayName: loc.dataController, value: this._controllerModel.controllerConfig?.metadata.name || '-' },
{ displayName: loc.region, value: azure?.location || '-' },
{ displayName: loc.namespace, value: this._postgresModel.config?.metadata.namespace || '-' },
{ displayName: loc.subscriptionId, value: azure?.subscription || '-' },
{ displayName: loc.externalEndpoint, value: this._postgresModel.config?.status.externalEndpoint || '-' },
{ displayName: loc.status, value: status ? `${status.state} (${status.readyPods} ${loc.podsReady})` : '-' },
{ displayName: loc.postgresAdminUsername, value: 'postgres' },
{ displayName: loc.dataController, value: this._controllerModel?.namespace ?? '' },
{ displayName: loc.nodeConfiguration, value: this._postgresModel.configuration },
{ displayName: loc.subscriptionId, value: registration?.subscriptionId ?? '' },
{ displayName: loc.postgresVersion, value: this._postgresModel.service?.spec?.engine?.version?.toString() ?? '' }
{ displayName: loc.postgresVersion, value: this._postgresModel.engineVersion ?? '-' },
{ displayName: loc.nodeConfiguration, value: this._postgresModel.scaleConfiguration || '-' }
];
*/
return [];
}
private getKibanaLink(): string {
const kibanaQuery = `kubernetes_namespace:"${this._postgresModel.namespace}" and custom_resource_name:"${this._postgresModel.name}"`;
return `${this._controllerModel.getEndpoint(Endpoints.logsui)?.endpoint}/app/kibana#/discover?_a=(query:(language:kuery,query:'${kibanaQuery}'))`;
private refreshDashboardLinks(): void {
if (this._postgresModel.config) {
const kibanaUrl = this._postgresModel.config.status.logSearchDashboard ?? '';
this.kibanaLink.label = kibanaUrl;
this.kibanaLink.url = kibanaUrl;
this.kibanaLoading.loading = false;
const grafanaUrl = this._postgresModel.config.status.metricsDashboard ?? '';
this.grafanaLink.label = grafanaUrl;
this.grafanaLink.url = grafanaUrl;
this.grafanaLoading.loading = false;
}
private getGrafanaLink(): string {
const grafanaQuery = `var-Namespace=${this._postgresModel.namespace}&var-Name=${this._postgresModel.name}`;
return `${this._controllerModel.getEndpoint(Endpoints.metricsui)?.endpoint}/d/postgres-metrics?${grafanaQuery}`;
}
private getNodes(): string[][] {
/* TODO chgagnon
const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint;
return this._postgresModel.pods?.map((pod: V1Pod) => {
const name = pod.metadata?.name ?? '';
const role: PodRole | undefined = PostgresModel.getPodRole(pod);
const service = pod.metadata?.annotations?.['arcdata.microsoft.com/serviceHost'];
const internalDns = service ? `${name}.${service}` : '';
return [
name,
PostgresModel.getPodRoleName(role),
PostgresModel.getPodStatus(pod),
role === PodRole.Router ? `${endpoint.ip}:${endpoint.port}` : internalDns
];
}) ?? [];
*/
return [];
}
private handleEndpointsUpdated() {
this.kibanaLink!.label = this.getKibanaLink();
this.kibanaLink!.url = this.getKibanaLink();
this.kibanaLoading!.loading = false;
this.grafanaLink!.label = this.getGrafanaLink();
this.grafanaLink!.url = this.getGrafanaLink();
this.grafanaLoading!.loading = false;
}
private handleRegistrationsUpdated() {
@@ -362,19 +286,9 @@ export class PostgresOverviewPage extends DashboardPage {
this.propertiesLoading!.loading = false;
}
private handleServiceUpdated() {
private handleConfigUpdated() {
this.properties!.propertyItems = this.getProperties();
this.propertiesLoading!.loading = false;
this.nodesTable!.data = this.getNodes();
this.nodesTableLoading!.loading = false;
}
private handlePodsUpdated() {
this.properties!.propertyItems = this.getProperties();
this.propertiesLoading!.loading = false;
this.nodesTable!.data = this.getNodes();
this.nodesTableLoading!.loading = false;
this.refreshDashboardLinks();
}
}

View File

@@ -7,7 +7,7 @@ import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as loc from '../../../localizedConstants';
import { IconPathHelper, cssStyles } from '../../../constants';
import { KeyValueContainer, KeyValue } from '../../components/keyValueContainer';
import { KeyValueContainer, KeyValue, InputKeyValue, TextKeyValue } from '../../components/keyValueContainer';
import { DashboardPage } from '../../components/dashboardPage';
import { ControllerModel } from '../../../models/controllerModel';
import { PostgresModel } from '../../../models/postgresModel';
@@ -19,7 +19,7 @@ export class PostgresPropertiesPage extends DashboardPage {
constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
super(modelView);
this.disposables.push(this._postgresModel.onServiceUpdated(
this.disposables.push(this._postgresModel.onConfigUpdated(
() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())));
this.disposables.push(this._controllerModel.onRegistrationsUpdated(
@@ -54,7 +54,7 @@ export class PostgresPropertiesPage extends DashboardPage {
this.loading = this.modelView.modelBuilder.loadingComponent()
.withItem(this.keyValueContainer.container)
.withProperties<azdata.LoadingComponentProperties>({
loading: !this._postgresModel.serviceLastUpdated && !this._controllerModel.registrationsLastUpdated
loading: !this._postgresModel.configLastUpdated && !this._controllerModel.registrationsLastUpdated
}).component();
content.addItem(this.loading);
@@ -91,24 +91,20 @@ export class PostgresPropertiesPage extends DashboardPage {
}
private getProperties(): KeyValue[] {
/*
const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint;
const connectionString = `postgresql://postgres@${endpoint.ip}:${endpoint.port}`;
const registration = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name);
const endpoint = this._postgresModel.endpoint;
const status = this._postgresModel.config?.status;
return [
new InputKeyValue(this.modelView.modelBuilder, loc.coordinatorEndpoint, connectionString),
new InputKeyValue(this.modelView.modelBuilder, loc.coordinatorEndpoint, endpoint ? `postgresql://postgres@${endpoint.ip}:${endpoint.port}` : ''),
new InputKeyValue(this.modelView.modelBuilder, loc.postgresAdminUsername, 'postgres'),
new TextKeyValue(this.modelView.modelBuilder, loc.status, this._postgresModel.service?.status?.state ?? 'Unknown'),
new TextKeyValue(this.modelView.modelBuilder, loc.status, status ? `${status.state} (${status.readyPods} ${loc.podsReady})` : loc.unknown),
// TODO: Make this a LinkKeyValue that opens the controller dashboard
new TextKeyValue(this.modelView.modelBuilder, loc.dataController, this._controllerModel.namespace ?? ''),
new TextKeyValue(this.modelView.modelBuilder, loc.nodeConfiguration, this._postgresModel.configuration),
new TextKeyValue(this.modelView.modelBuilder, loc.postgresVersion, this._postgresModel.service?.spec?.engine?.version?.toString() ?? ''),
new TextKeyValue(this.modelView.modelBuilder, loc.resourceGroup, registration?.resourceGroupName ?? ''),
new TextKeyValue(this.modelView.modelBuilder, loc.subscriptionId, registration?.subscriptionId ?? '')
new TextKeyValue(this.modelView.modelBuilder, loc.dataController, this._controllerModel.controllerConfig?.metadata.namespace ?? ''),
new TextKeyValue(this.modelView.modelBuilder, loc.nodeConfiguration, this._postgresModel.scaleConfiguration ?? ''),
new TextKeyValue(this.modelView.modelBuilder, loc.postgresVersion, this._postgresModel.engineVersion ?? ''),
new TextKeyValue(this.modelView.modelBuilder, loc.resourceGroup, this._controllerModel.controllerConfig?.spec.settings.azure.resourceGroup ?? ''),
new TextKeyValue(this.modelView.modelBuilder, loc.subscriptionId, this._controllerModel.controllerConfig?.spec.settings.azure.subscription ?? '')
];
*/
return [];
}
private handleRegistrationsUpdated() {

View File

@@ -1,225 +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 vscode from 'vscode';
import * as azdata from 'azdata';
import * as loc from '../../../localizedConstants';
import { IconPathHelper, cssStyles } from '../../../constants';
import { DashboardPage } from '../../components/dashboardPage';
import { PostgresModel } from '../../../models/postgresModel';
import { fromNow } from '../../../common/date';
export class PostgresResourceHealthPage extends DashboardPage {
private interval: NodeJS.Timeout;
private podsUpdated?: azdata.TextComponent;
private podsLoading?: azdata.LoadingComponent;
private conditionsLoading?: azdata.LoadingComponent;
private podsTable?: azdata.DeclarativeTableComponent;
private conditionsTable?: azdata.DeclarativeTableComponent;
constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) {
super(modelView);
this.disposables.push(
modelView.onClosed(() => {
try { clearInterval(this.interval); }
catch { }
}));
this.disposables.push(this._postgresModel.onServiceUpdated(
() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())));
// Keep the last updated timestamps up to date with the current time
this.interval = setInterval(() => this.handleServiceUpdated(), 60 * 1000);
}
protected get title(): string {
return loc.resourceHealth;
}
protected get id(): string {
return 'postgres-resource-health';
}
protected get icon(): { dark: string; light: string; } {
return IconPathHelper.health;
}
protected get container(): azdata.Component {
const root = this.modelView.modelBuilder.divContainer().component();
const content = this.modelView.modelBuilder.divContainer().component();
root.addItem(content, { CSSStyles: { 'margin': '20px' } });
content.addItem(this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: loc.resourceHealth,
CSSStyles: { ...cssStyles.title, 'margin-bottom': '30px' }
}).component());
content.addItem(this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: loc.podOverview,
CSSStyles: { ...cssStyles.title, 'margin-block-end': '0' }
}).component());
this.podsUpdated = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: this.getPodsLastUpdated(),
CSSStyles: { ...cssStyles.text, 'font-size': '12px', 'margin-block-start': '0' }
}).component();
content.addItem(this.podsUpdated);
// Pod overview
this.podsTable = this.modelView.modelBuilder.declarativeTable().withProperties<azdata.DeclarativeTableProperties>({
columns: [
{
displayName: '',
valueType: azdata.DeclarativeDataType.string,
width: '35%',
isReadOnly: true,
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: { ...cssStyles.tableRow, 'font-size': '20px', 'font-weight': 'bold', 'padding': '7px' }
},
{
displayName: '',
valueType: azdata.DeclarativeDataType.string,
width: '65%',
isReadOnly: true,
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: { ...cssStyles.tableRow, 'padding': '7px' }
}
],
data: this.getPodsTable()
}).component();
this.podsLoading = this.modelView.modelBuilder.loadingComponent()
.withItem(this.podsTable)
.withProperties<azdata.LoadingComponentProperties>({
loading: !this._postgresModel.serviceLastUpdated
}).component();
content.addItem(this.podsLoading, { CSSStyles: { 'margin-bottom': '30px' } });
// Conditions table
this.conditionsTable = this.modelView.modelBuilder.declarativeTable().withProperties<azdata.DeclarativeTableProperties>({
width: '100%',
columns: [
{
displayName: loc.condition,
valueType: azdata.DeclarativeDataType.string,
width: '15%',
isReadOnly: true,
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: cssStyles.tableRow
},
{
displayName: '',
valueType: azdata.DeclarativeDataType.component,
width: '1%',
isReadOnly: true,
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: cssStyles.tableRow
},
{
displayName: loc.details,
valueType: azdata.DeclarativeDataType.string,
width: '64%',
isReadOnly: true,
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: cssStyles.tableRow
},
{
displayName: loc.lastUpdated,
valueType: azdata.DeclarativeDataType.string,
width: '20%',
isReadOnly: true,
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: { ...cssStyles.tableRow, 'white-space': 'nowrap' }
}
],
data: this.getConditionsTable()
}).component();
this.conditionsLoading = this.modelView.modelBuilder.loadingComponent()
.withItem(this.conditionsTable)
.withProperties<azdata.LoadingComponentProperties>({
loading: !this._postgresModel.serviceLastUpdated
}).component();
content.addItem(this.conditionsLoading);
this.initialized = true;
return root;
}
protected get toolbarContainer(): azdata.ToolbarContainer {
const refreshButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
label: loc.refresh,
iconPath: IconPathHelper.refresh
}).component();
this.disposables.push(
refreshButton.onDidClick(async () => {
refreshButton.enabled = false;
try {
this.podsLoading!.loading = true;
this.conditionsLoading!.loading = true;
await this._postgresModel.refresh();
} catch (error) {
vscode.window.showErrorMessage(loc.refreshFailed(error));
} finally {
refreshButton.enabled = true;
}
}));
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([
{ component: refreshButton }
]).component();
}
private getPodsLastUpdated(): string {
return this._postgresModel.serviceLastUpdated
? loc.updated(fromNow(this._postgresModel.serviceLastUpdated!, true)) : '';
}
private getPodsTable(): (string | number)[][] {
return [
[this._postgresModel.service?.status?.podsRunning ?? 0, loc.running],
[this._postgresModel.service?.status?.podsPending ?? 0, loc.pending],
[this._postgresModel.service?.status?.podsFailed ?? 0, loc.failed],
[this._postgresModel.service?.status?.podsUnknown ?? 0, loc.unknown]
];
}
private getConditionsTable(): (string | azdata.ImageComponent)[][] {
/* TODO chgagnon
return this._postgresModel.service?.status?.conditions?.map(c => {
const healthy = c.type === 'Ready' ? c.status === 'True' : c.status === 'False';
const image = this.modelView.modelBuilder.image().withProperties<azdata.ImageComponentProperties>({
iconPath: healthy ? IconPathHelper.success : IconPathHelper.fail,
iconHeight: '20px',
iconWidth: '20px',
width: '20px',
height: '20px'
}).component();
return [
c.type ?? '',
image,
c.message ?? '',
c.lastTransitionTime ? fromNow(c.lastTransitionTime!, true) : ''
];
}) ?? [];
*/
return [];
}
private handleServiceUpdated() {
this.podsUpdated!.value = this.getPodsLastUpdated();
this.podsTable!.data = this.getPodsTable();
this.podsLoading!.loading = false;
this.conditionsTable!.data = this.getConditionsTable();
this.conditionsLoading!.loading = false;
}
}

View File

@@ -3,13 +3,17 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as loc from '../../../localizedConstants';
import { IconPathHelper, cssStyles } from '../../../constants';
import { DashboardPage } from '../../components/dashboardPage';
import { ControllerModel } from '../../../models/controllerModel';
import { ResourceType } from 'arc';
import { PostgresModel } from '../../../models/postgresModel';
export class PostgresSupportRequestPage extends DashboardPage {
constructor(protected modelView: azdata.ModelView) {
constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
super(modelView);
}
@@ -48,15 +52,13 @@ export class PostgresSupportRequestPage extends DashboardPage {
this.disposables.push(
supportRequestButton.onDidClick(() => {
/*
const r = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name);
if (!r) {
vscode.window.showErrorMessage(loc.couldNotFindAzureResource(this._postgresModel.fullName));
} else {
const azure = this._controllerModel.controllerConfig?.spec.settings.azure;
if (azure) {
vscode.env.openExternal(vscode.Uri.parse(
`https://portal.azure.com/#resource/subscriptions/${r.subscriptionId}/resourceGroups/${r.resourceGroupName}/providers/Microsoft.AzureData/${ResourceType.postgresInstances}/${r.instanceName}/supportrequest`));
`https://portal.azure.com/#resource/subscriptions/${azure.subscription}/resourceGroups/${azure.resourceGroup}/providers/Microsoft.AzureData/${ResourceType.postgresInstances}/${this._postgresModel.info.name}/supportrequest`));
} else {
vscode.window.showErrorMessage(loc.couldNotFindControllerRegistration);
}
*/
}));
content.addItem(supportRequestButton);

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ControllerInfo } from 'arc';
import { ControllerInfo, ResourceInfo } from 'arc';
import * as azdata from 'azdata';
import * as azdataExt from 'azdata-ext';
import { v4 as uuid } from 'uuid';
@@ -74,6 +74,7 @@ abstract class ControllerDialogBase extends InitializingComponent {
protected completionPromise = new Deferred<ConnectToControllerDialogModel | undefined>();
protected id!: string;
protected resources: ResourceInfo[] = [];
constructor(protected treeDataProvider: AzureArcTreeDataProvider, title: string) {
super();
@@ -82,6 +83,7 @@ abstract class ControllerDialogBase extends InitializingComponent {
public showDialog(controllerInfo?: ControllerInfo, password: string | undefined = undefined): azdata.window.Dialog {
this.id = controllerInfo?.id ?? uuid();
this.resources = controllerInfo?.resources ?? [];
this.dialog.cancelButton.onClick(() => this.handleCancel());
this.dialog.registerContent(async (view) => {
this.modelBuilder = view.modelBuilder;
@@ -168,7 +170,7 @@ export class ConnectToControllerDialog extends ControllerDialogBase {
name: this.nameInputBox.value ?? '',
username: this.usernameInputBox.value,
rememberPassword: this.rememberPwCheckBox.checked ?? false,
resources: []
resources: this.resources
};
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
try {

View File

@@ -0,0 +1,135 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { Deferred } from '../../common/promise';
import { createCredentialId } from '../../common/utils';
import { credentialNamespace } from '../../constants';
import * as loc from '../../localizedConstants';
import { ControllerModel } from '../../models/controllerModel';
import { MiaaModel } from '../../models/miaaModel';
import { InitializingComponent } from '../components/initializingComponent';
export class ConnectToSqlDialog extends InitializingComponent {
private modelBuilder!: azdata.ModelBuilder;
private serverNameInputBox!: azdata.InputBoxComponent;
private usernameInputBox!: azdata.InputBoxComponent;
private passwordInputBox!: azdata.InputBoxComponent;
private rememberPwCheckBox!: azdata.CheckBoxComponent;
private _completionPromise = new Deferred<azdata.IConnectionProfile | undefined>();
constructor(private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) {
super();
}
public showDialog(connectionProfile?: azdata.IConnectionProfile): azdata.window.Dialog {
const dialog = azdata.window.createModelViewDialog(loc.connectToSql(this._miaaModel.info.name));
dialog.cancelButton.onClick(() => this.handleCancel());
dialog.registerContent(async view => {
this.modelBuilder = view.modelBuilder;
this.serverNameInputBox = this.modelBuilder.inputBox()
.withProperties<azdata.InputBoxProperties>({
value: connectionProfile?.serverName,
enabled: false
}).component();
this.usernameInputBox = this.modelBuilder.inputBox()
.withProperties<azdata.InputBoxProperties>({
value: connectionProfile?.userName
}).component();
this.passwordInputBox = this.modelBuilder.inputBox()
.withProperties<azdata.InputBoxProperties>({
inputType: 'password',
value: connectionProfile?.password
})
.component();
this.rememberPwCheckBox = this.modelBuilder.checkBox()
.withProperties<azdata.CheckBoxProperties>({
label: loc.rememberPassword,
checked: connectionProfile?.savePassword
}).component();
let formModel = this.modelBuilder.formContainer()
.withFormItems([{
components: [
{
component: this.serverNameInputBox,
title: loc.serverEndpoint,
required: true
}, {
component: this.usernameInputBox,
title: loc.username,
required: true
}, {
component: this.passwordInputBox,
title: loc.password,
required: true
}, {
component: this.rememberPwCheckBox,
title: ''
}
],
title: ''
}]).withLayout({ width: '100%' }).component();
await view.initializeModel(formModel);
this.serverNameInputBox.focus();
this.initialized = true;
});
dialog.registerCloseValidator(async () => await this.validate());
dialog.okButton.label = loc.connect;
dialog.cancelButton.label = loc.cancel;
azdata.window.openDialog(dialog);
return dialog;
}
public async validate(): Promise<boolean> {
if (!this.serverNameInputBox.value || !this.usernameInputBox.value || !this.passwordInputBox.value) {
return false;
}
const connectionProfile: azdata.IConnectionProfile = {
serverName: this.serverNameInputBox.value,
databaseName: '',
authenticationType: 'SqlLogin',
providerName: 'MSSQL',
connectionName: '',
userName: this.usernameInputBox.value,
password: this.passwordInputBox.value,
savePassword: !!this.rememberPwCheckBox.checked,
groupFullName: undefined,
saveProfile: true,
id: '',
groupId: undefined,
options: {}
};
const result = await azdata.connection.connect(connectionProfile, false, false);
if (result.connected) {
connectionProfile.id = result.connectionId;
const credentialProvider = await azdata.credentials.getProvider(credentialNamespace);
if (connectionProfile.savePassword) {
await credentialProvider.saveCredential(createCredentialId(this._controllerModel.info.id, this._miaaModel.info.resourceType, this._miaaModel.info.name), connectionProfile.password);
} else {
await credentialProvider.deleteCredential(createCredentialId(this._controllerModel.info.id, this._miaaModel.info.resourceType, this._miaaModel.info.name));
}
this._completionPromise.resolve(connectionProfile);
return true;
}
else {
vscode.window.showErrorMessage(loc.connectToSqlFailed(this.serverNameInputBox.value, result.errorMessage));
return false;
}
}
private handleCancel(): void {
this._completionPromise.resolve(undefined);
}
public waitForClose(): Promise<azdata.IConnectionProfile | undefined> {
return this._completionPromise.promise;
}
}

View File

@@ -135,10 +135,14 @@ export class AzureArcTreeDataProvider implements vscode.TreeDataProvider<TreeNod
if (resourceNode) {
await resourceNode.openDashboard();
} else {
console.log(`Couldn't find resource node for ${name} (${resourceType})`);
const errMsg = `Couldn't find resource node for ${name} (${resourceType})`;
console.log(errMsg);
throw new Error(errMsg);
}
} else {
console.log('Couldn\'t find controller node for opening dashboard');
const errMsg = 'Couldn\'t find controller node for opening dashboard';
console.log(errMsg);
throw new Error(errMsg);
}
}
}

View File

@@ -3,16 +3,18 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ResourceInfo, ResourceType } from 'arc';
import { MiaaResourceInfo, ResourceInfo, ResourceType } from 'arc';
import * as vscode from 'vscode';
import { parseInstanceName, UserCancelledError } from '../../common/utils';
import { UserCancelledError } from '../../common/utils';
import * as loc from '../../localizedConstants';
import { ControllerModel, Registration } from '../../models/controllerModel';
import { MiaaModel } from '../../models/miaaModel';
import { PostgresModel } from '../../models/postgresModel';
import { ResourceModel } from '../../models/resourceModel';
import { ControllerDashboard } from '../dashboards/controller/controllerDashboard';
import { AzureArcTreeDataProvider } from './azureArcTreeDataProvider';
import { MiaaTreeNode } from './miaaTreeNode';
import { NoInstancesTreeNode } from './noInstancesTreeNode';
import { PostgresTreeNode } from './postgresTreeNode';
import { RefreshTreeNode } from './refreshTreeNode';
import { ResourceTreeNode } from './resourceTreeNode';
@@ -23,7 +25,7 @@ import { TreeNode } from './treeNode';
*/
export class ControllerTreeNode extends TreeNode {
private _children: ResourceTreeNode[] = [];
private _children: ResourceTreeNode<ResourceModel>[] = [];
constructor(public model: ControllerModel, private _context: vscode.ExtensionContext, private _treeDataProvider: AzureArcTreeDataProvider) {
super(model.label, vscode.TreeItemCollapsibleState.Collapsed, ResourceType.dataControllers);
@@ -55,7 +57,7 @@ export class ControllerTreeNode extends TreeNode {
}
}
return this._children;
return this._children.length > 0 ? this._children : [new NoInstancesTreeNode()];
}
public async openDashboard(): Promise<void> {
@@ -68,14 +70,14 @@ export class ControllerTreeNode extends TreeNode {
* @param resourceType The resourceType of the node
* @param name The name of the node
*/
public getResourceNode(resourceType: string, name: string): ResourceTreeNode | undefined {
public getResourceNode(resourceType: string, name: string): ResourceTreeNode<ResourceModel> | undefined {
return this._children.find(c =>
c.model?.info.resourceType === resourceType &&
c.model.info.name === name);
}
private updateChildren(registrations: Registration[]): void {
const newChildren: ResourceTreeNode[] = [];
const newChildren: ResourceTreeNode<ResourceModel>[] = [];
registrations.forEach(registration => {
if (!registration.instanceName) {
console.warn('Registration is missing required name value, skipping');
@@ -83,7 +85,7 @@ export class ControllerTreeNode extends TreeNode {
}
const resourceInfo: ResourceInfo = {
name: parseInstanceName(registration.instanceName),
name: registration.instanceName,
resourceType: registration.instanceType ?? ''
};
@@ -100,10 +102,14 @@ export class ControllerTreeNode extends TreeNode {
switch (registration.instanceType) {
case ResourceType.postgresInstances:
const postgresModel = new PostgresModel(resourceInfo, registration);
const postgresModel = new PostgresModel(this.model, resourceInfo, registration);
node = new PostgresTreeNode(postgresModel, this.model, this._context);
break;
case ResourceType.sqlManagedInstances:
// Fill in the username too if we already have it
(resourceInfo as MiaaResourceInfo).userName = (this.model.info.resources.find(info =>
info.name === resourceInfo.name &&
info.resourceType === resourceInfo.resourceType) as MiaaResourceInfo)?.userName;
const miaaModel = new MiaaModel(this.model, resourceInfo, registration, this._treeDataProvider);
node = new MiaaTreeNode(miaaModel, this.model);
break;

View File

@@ -8,15 +8,15 @@ import * as vscode from 'vscode';
import { ControllerModel } from '../../models/controllerModel';
import { MiaaModel } from '../../models/miaaModel';
import { MiaaDashboard } from '../dashboards/miaa/miaaDashboard';
import { TreeNode } from './treeNode';
import { ResourceTreeNode } from './resourceTreeNode';
/**
* The TreeNode for displaying a SQL Managed Instance on Azure Arc
*/
export class MiaaTreeNode extends TreeNode {
export class MiaaTreeNode extends ResourceTreeNode<MiaaModel> {
constructor(public model: MiaaModel, private _controllerModel: ControllerModel) {
super(model.info.name, vscode.TreeItemCollapsibleState.None, ResourceType.sqlManagedInstances);
constructor(model: MiaaModel, private _controllerModel: ControllerModel) {
super(model.info.name, vscode.TreeItemCollapsibleState.None, ResourceType.sqlManagedInstances, model);
}
public async openDashboard(): Promise<void> {

View File

@@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* 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 loc from '../../localizedConstants';
import { TreeNode } from './treeNode';
/**
* A placeholder TreeNode to display when there aren't any child instances available
*/
export class NoInstancesTreeNode extends TreeNode {
constructor() {
super(loc.noInstancesAvailable, vscode.TreeItemCollapsibleState.None, '');
}
}

View File

@@ -13,14 +13,14 @@ import { ResourceTreeNode } from './resourceTreeNode';
/**
* The TreeNode for displaying an Postgres Server group
*/
export class PostgresTreeNode extends ResourceTreeNode {
export class PostgresTreeNode extends ResourceTreeNode<PostgresModel> {
constructor(private _model: PostgresModel, private _controllerModel: ControllerModel, private _context: vscode.ExtensionContext) {
super(_model.name, vscode.TreeItemCollapsibleState.None, ResourceType.postgresInstances, _model);
constructor(model: PostgresModel, private _controllerModel: ControllerModel, private _context: vscode.ExtensionContext) {
super(model.info.name, vscode.TreeItemCollapsibleState.None, ResourceType.postgresInstances, model);
}
public async openDashboard(): Promise<void> {
const postgresDashboard = new PostgresDashboard(this._context, this._controllerModel, this._model);
const postgresDashboard = new PostgresDashboard(this._context, this._controllerModel, this.model);
await postgresDashboard.showDashboard();
}
}

View File

@@ -14,7 +14,7 @@ import { refreshActionId } from '../../constants';
export class RefreshTreeNode extends TreeNode {
constructor(private _parent: TreeNode) {
super(loc.refreshToEnterCredentials, vscode.TreeItemCollapsibleState.None, 'refresh');
super(loc.refreshToEnterCredentials, vscode.TreeItemCollapsibleState.None, '');
}
public command: vscode.Command = {

View File

@@ -10,8 +10,8 @@ import { TreeNode } from './treeNode';
/**
* A TreeNode belonging to a child of a Controller
*/
export abstract class ResourceTreeNode extends TreeNode {
constructor(label: string, collapsibleState: vscode.TreeItemCollapsibleState, resourceType?: string, public model?: ResourceModel) {
export abstract class ResourceTreeNode<M extends ResourceModel> extends TreeNode {
constructor(label: string, collapsibleState: vscode.TreeItemCollapsibleState, resourceType: string, public model: M) {
super(label, collapsibleState, resourceType);
}
}

View File

@@ -2,7 +2,7 @@
Welcome to Microsoft Azure Data CLI Extension for Azure Data Studio!
**This extension is only applicable to customers in the Azure Arc data services private preview. Other usage is not supported at this time.**
**This extension is only applicable to customers in the Azure Arc data services public preview. Other usage is not supported at this time.**
## Overview

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -2,14 +2,14 @@
"name": "azdata",
"displayName": "%azdata.displayName%",
"description": "%azdata.description%",
"version": "0.1.2",
"version": "0.3.1",
"publisher": "Microsoft",
"preview": true,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
"icon": "images/extension.png",
"engines": {
"vscode": "*",
"azdata": ">=1.20.0"
"azdata": ">=1.22.0"
},
"activationEvents": [
"*"
@@ -132,5 +132,10 @@
"sinon": "^9.0.2",
"typemoq": "^2.1.0",
"vscodetestcover": "^1.1.0"
},
"__metadata": {
"id": "73",
"publisherDisplayName": "Microsoft",
"publisherId": "Microsoft"
}
}

View File

@@ -72,6 +72,11 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
},
postgres: {
server: {
delete: async (name: string) => {
await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.postgres.server.delete(name);
},
list: async () => {
await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
@@ -81,6 +86,26 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.postgres.server.show(name);
},
edit: async (
name: string,
args: {
adminPassword?: boolean;
coresLimit?: string;
coresRequest?: string;
engineSettings?: string;
extensions?: string;
memoryLimit?: string;
memoryRequest?: string;
noWait?: boolean;
port?: number;
replaceEngineSettings?: boolean;
workers?: number;
},
additionalEnvVars?: { [key: string]: string; }) => {
await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.postgres.server.edit(name, args, additionalEnvVars);
}
}
},

View File

@@ -6,13 +6,15 @@
import * as azdataExt from 'azdata-ext';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { SemVer } from 'semver';
import * as vscode from 'vscode';
import { getPlatformDownloadLink, getPlatformReleaseVersion } from './azdataReleaseInfo';
import { executeCommand, executeSudoCommand, ExitCodeError, ProcessOutput } from './common/childProcess';
import { HttpClient } from './common/httpClient';
import Logger from './common/logger';
import { getErrorMessage, searchForCmd } from './common/utils';
import { azdataAcceptEulaKey, azdataConfigSection, azdataFound, azdataHostname, azdataInstallKey, azdataReleaseJson, azdataUpdateKey, azdataUri, debugConfigKey, eulaAccepted, eulaUrl, microsoftPrivacyStatementUrl } from './constants';
import { getErrorMessage, NoAzdataError, searchForCmd } from './common/utils';
import { azdataAcceptEulaKey, azdataConfigSection, azdataFound, azdataInstallKey, azdataUpdateKey, debugConfigKey, eulaAccepted, eulaUrl, microsoftPrivacyStatementUrl } from './constants';
import * as loc from './localizedConstants';
const enum AzdataDeployOption {
@@ -92,11 +94,44 @@ export class AzdataTool implements azdataExt.IAzdataApi {
},
postgres: {
server: {
delete: async (name: string) => {
return this.executeCommand<void>(['arc', 'postgres', 'server', 'delete', '-n', name]);
},
list: async () => {
return this.executeCommand<azdataExt.PostgresServerListResult[]>(['arc', 'postgres', 'server', 'list']);
},
show: async (name: string) => {
return this.executeCommand<azdataExt.PostgresServerShowResult>(['arc', 'postgres', 'server', 'show', '-n', name]);
},
edit: async (
name: string,
args: {
adminPassword?: boolean,
coresLimit?: string,
coresRequest?: string,
engineSettings?: string,
extensions?: string,
memoryLimit?: string,
memoryRequest?: string,
noWait?: boolean,
port?: number,
replaceEngineSettings?: boolean,
workers?: number
},
additionalEnvVars?: { [key: string]: string }) => {
const argsArray = ['arc', 'postgres', 'server', 'edit', '-n', name];
if (args.adminPassword) { argsArray.push('--admin-password'); }
if (args.coresLimit !== undefined) { argsArray.push('--cores-limit', args.coresLimit); }
if (args.coresRequest !== undefined) { argsArray.push('--cores-request', args.coresRequest); }
if (args.engineSettings !== undefined) { argsArray.push('--engine-settings', args.engineSettings); }
if (args.extensions !== undefined) { argsArray.push('--extensions', args.extensions); }
if (args.memoryLimit !== undefined) { argsArray.push('--memory-limit', args.memoryLimit); }
if (args.memoryRequest !== undefined) { argsArray.push('--memory-request', args.memoryRequest); }
if (args.noWait) { argsArray.push('--no-wait'); }
if (args.port !== undefined) { argsArray.push('--port', args.port.toString()); }
if (args.replaceEngineSettings) { argsArray.push('--replace-engine-settings'); }
if (args.workers !== undefined) { argsArray.push('--workers', args.workers.toString()); }
return this.executeCommand<void>(argsArray, additionalEnvVars);
}
}
},
@@ -151,18 +186,18 @@ export class AzdataTool implements azdataExt.IAzdataApi {
// ERROR: { stderr: '...' }
// so we also need to trim off the start that isn't a valid JSON blob
err.stderr = JSON.parse(err.stderr.substring(err.stderr.indexOf('{'), err.stderr.indexOf('}') + 1)).stderr;
} catch (err) {
} catch {
// it means this was probably some other generic error (such as command not being found)
// check if azdata still exists if it does then rethrow the original error if not then emit a new specific error.
try {
await fs.promises.access(this._path);
//this.path exists
throw err; // rethrow the error
} catch (e) {
// this.path does not exist
await vscode.commands.executeCommand('setContext', azdataFound, false);
throw (loc.noAzdata);
throw new NoAzdataError();
}
throw err; // rethrow the original error
}
}
@@ -311,6 +346,7 @@ async function promptToInstallAzdata(userRequested: boolean = false): Promise<bo
? [loc.yes, loc.no]
: [loc.yes, loc.askLater, loc.doNotAskAgain];
if (config === AzdataDeployOption.prompt) {
Logger.log(loc.promptForAzdataInstallLog);
response = await vscode.window.showErrorMessage(loc.promptForAzdataInstall, ...responses);
Logger.log(loc.userResponseToInstallPrompt(response));
}
@@ -354,6 +390,7 @@ async function promptToUpdateAzdata(newVersion: string, userRequested: boolean =
? [loc.yes, loc.no]
: [loc.yes, loc.askLater, loc.doNotAskAgain];
if (config === AzdataDeployOption.prompt) {
Logger.log(loc.promptForAzdataUpdateLog(newVersion));
response = await vscode.window.showInformationMessage(loc.promptForAzdataUpdate(newVersion), ...responses);
Logger.log(loc.userResponseToUpdatePrompt(response));
}
@@ -427,9 +464,16 @@ export async function promptForEula(memento: vscode.Memento, userRequested: bool
* Downloads the Windows installer and runs it
*/
async function downloadAndInstallAzdataWin32(): Promise<void> {
const downLoadLink = await getPlatformDownloadLink();
const downloadFolder = os.tmpdir();
const downloadedFile = await HttpClient.downloadFile(`${azdataHostname}/${azdataUri}`, downloadFolder);
await executeCommand('msiexec', ['/qn', '/i', downloadedFile]);
const downloadLogs = path.join(downloadFolder, 'ads_azdata_install_logs.log');
const downloadedFile = await HttpClient.downloadFile(downLoadLink, downloadFolder);
try {
await executeSudoCommand(`msiexec /qn /i "${downloadedFile}" /lvx "${downloadLogs}"`);
} catch (err) {
throw new Error(`${err.message}. See logs at ${downloadLogs} for more details.`);
}
}
/**
@@ -502,27 +546,10 @@ export async function discoverLatestAvailableAzdataVersion(): Promise<SemVer> {
// However, doing discovery that way required apt update to be performed which requires sudo privileges. At least currently this code path
// gets invoked on extension start up and prompt user for sudo privileges is annoying at best. So for now basing linux discovery also on a releaseJson file.
default:
return await discoverLatestAzdataVersionFromJson();
return await getPlatformReleaseVersion();
}
}
/**
* Gets the latest azdata version from a json document published by azdata release
*/
async function discoverLatestAzdataVersionFromJson(): Promise<SemVer> {
// get version information for current platform from http://aka.ms/azdata/release.json
const fileContents = await HttpClient.getTextContent(`${azdataHostname}/${azdataReleaseJson}`);
let azdataReleaseInfo;
try {
azdataReleaseInfo = JSON.parse(fileContents);
} catch (e) {
throw Error(`failed to parse the JSON of contents at: ${azdataHostname}/${azdataReleaseJson}, text being parsed: '${fileContents}', error:${getErrorMessage(e)}`);
}
const version = azdataReleaseInfo[process.platform]['version'];
Logger.log(loc.latestAzdataVersionAvailable(version));
return new SemVer(version);
}
/**
* Parses out the azdata version from the raw azdata version output
* @param raw The raw version output from azdata --version

View File

@@ -0,0 +1,76 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as os from 'os';
import * as loc from './localizedConstants';
import { SemVer } from 'semver';
import { HttpClient } from './common/httpClient';
import Logger from './common/logger';
import { getErrorMessage } from './common/utils';
import { azdataHostname, azdataReleaseJson } from './constants';
interface PlatformReleaseInfo {
version: string; // "20.0.1"
link?: string; // "https://aka.ms/azdata-msi"
}
interface AzdataReleaseInfo {
win32: PlatformReleaseInfo,
darwin: PlatformReleaseInfo,
linux: PlatformReleaseInfo
}
function getPlatformAzdataReleaseInfo(releaseInfo: AzdataReleaseInfo): PlatformReleaseInfo {
switch (os.platform()) {
case 'win32':
return releaseInfo.win32;
case 'linux':
return releaseInfo.linux;
case 'darwin':
return releaseInfo.darwin;
default:
Logger.log(loc.platformUnsupported(os.platform()));
throw new Error(`Unsupported AzdataReleaseInfo platform '${os.platform()}`);
}
}
/**
* Gets the release version for the current platform from the release info - throwing an error if it doesn't exist.
* @param releaseInfo The AzdataReleaseInfo object
*/
export async function getPlatformReleaseVersion(): Promise<SemVer> {
const releaseInfo = await getAzdataReleaseInfo();
const platformReleaseInfo = getPlatformAzdataReleaseInfo(releaseInfo);
if (!platformReleaseInfo.version) {
Logger.log(loc.noReleaseVersion(os.platform(), JSON.stringify(releaseInfo)));
throw new Error(`No release version available for platform ${os.platform()}`);
}
Logger.log(loc.latestAzdataVersionAvailable(platformReleaseInfo.version));
return new SemVer(platformReleaseInfo.version);
}
/**
* Gets the download link for the current platform from the release info - throwing an error if it doesn't exist.
* @param releaseInfo The AzdataReleaseInfo object
*/
export async function getPlatformDownloadLink(): Promise<string> {
const releaseInfo = await getAzdataReleaseInfo();
const platformReleaseInfo = getPlatformAzdataReleaseInfo(releaseInfo);
if (!platformReleaseInfo.link) {
Logger.log(loc.noDownloadLink(os.platform(), JSON.stringify(releaseInfo)));
throw new Error(`No download link available for platform ${os.platform()}`);
}
return platformReleaseInfo.link;
}
async function getAzdataReleaseInfo(): Promise<AzdataReleaseInfo> {
const fileContents = await HttpClient.getTextContent(`${azdataHostname}/${azdataReleaseJson}`);
try {
return JSON.parse(fileContents);
} catch (e) {
Logger.log(loc.failedToParseReleaseInfo(`${azdataHostname}/${azdataReleaseJson}`, fileContents, e));
throw Error(`Failed to parse the JSON of contents at: ${azdataHostname}/${azdataReleaseJson}. Error: ${getErrorMessage(e)}`);
}
}

View File

@@ -65,7 +65,7 @@ export async function executeCommand(command: string, args: string[], additional
Logger.log(loc.stdoutOutput(stdout));
}
if (stderr) {
Logger.log(loc.stdoutOutput(stderr));
Logger.log(loc.stderrOutput(stderr));
}
if (code) {
const err = new ExitCodeError(code, stderr);
@@ -94,7 +94,7 @@ export async function executeSudoCommand(command: string): Promise<ProcessOutput
Logger.log(loc.stdoutOutput(stdout));
}
if (stderr) {
Logger.log(loc.stdoutOutput(stderr));
Logger.log(loc.stderrOutput(stderr));
}
if (error) {
Logger.log(loc.unexpectedCommandError(error.message));

View File

@@ -61,7 +61,7 @@ export namespace HttpClient {
if (targetFolder !== undefined) {
const filename = path.basename(response.request.path);
const targetPath = path.join(targetFolder, filename);
Logger.log(loc.downloadingTo(filename, targetPath));
Logger.log(loc.downloadingTo(filename, downloadUrl, targetPath));
// Wait to create the WriteStream until here so we can use the actual
// filename based off of the URI.
downloadRequest.pipe(fs.createWriteStream(targetPath))
@@ -73,6 +73,11 @@ export namespace HttpClient {
reject(downloadError);
downloadRequest.abort();
});
} else {
response.on('end', () => {
Logger.log(loc.downloadFinished);
resolve(strings.join(''));
});
}
let contentLength = response.headers['content-length'];
let totalBytes = parseInt(contentLength || '0');
@@ -92,13 +97,6 @@ export namespace HttpClient {
printThreshold += 0.1;
}
}
})
.on('close', async () => {
if (targetFolder === undefined) {
Logger.log(loc.downloadFinished);
resolve(strings.join(''));
}
});
});
}

View File

@@ -4,16 +4,17 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as loc from '../localizedConstants';
export class Log {
private _output: vscode.OutputChannel;
constructor() {
this._output = vscode.window.createOutputChannel('azdata');
this._output = vscode.window.createOutputChannel(loc.azdata);
}
log(msg: string): void {
this._output.appendLine(msg);
this._output.appendLine(`[${new Date().toISOString()}] ${msg}`);
}
show(): void {

View File

@@ -7,7 +7,6 @@ import * as azdataExt from 'azdata-ext';
import * as which from 'which';
import * as loc from '../localizedConstants';
export class NoAzdataError extends Error implements azdataExt.ErrorWithLink {
constructor() {
super(loc.noAzdata);
@@ -17,7 +16,6 @@ export class NoAzdataError extends Error implements azdataExt.ErrorWithLink {
return loc.noAzdataWithLink;
}
}
/**
* Searches for the first instance of the specified executable in the PATH environment variable
* @param exe The executable to search for

View File

@@ -34,15 +34,6 @@ export async function activate(context: vscode.ExtensionContext): Promise<azdata
eulaAccepted = isEulaAccepted(context.globalState); // fetch eula acceptance state from memento
await vscode.commands.executeCommand('setContext', constants.eulaAccepted, eulaAccepted); // set a context key for current value of eulaAccepted state retrieved from memento so that command for accepting eula is available/unavailable in commandPalette appropriately.
Logger.log(loc.eulaAcceptedStateOnStartup(eulaAccepted));
if (!eulaAccepted) {
// Don't block on this since we want extension to finish activating without requiring user actions.
// If EULA has not been accepted then we will check again while executing azdata commands.
promptForEula(context.globalState)
.then(async (userResponse: boolean) => {
eulaAccepted = userResponse;
})
.catch((err) => console.log(err));
}
// Don't block on this since we want the extension to finish activating without needing user input
const localAzdataDiscovered = checkAndInstallAzdata() // install if not installed and user wants it.

View File

@@ -4,16 +4,18 @@
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
import { getErrorMessage } from './common/utils';
import { azdataConfigSection, azdataInstallKey, azdataUpdateKey } from './constants';
const localize = nls.loadMessageBundle();
export const azdata = localize('azdata.azdata', "Azure Data CLI");
export const searchingForAzdata = localize('azdata.searchingForAzdata', "Searching for existing Azure Data CLI installation...");
export const foundExistingAzdata = (path: string, version: string): string => localize('azdata.foundExistingAzdata', "Found existing Azure Data CLI installation of version (v{0}) at path:{1}", version, path);
export const downloadingProgressMb = (currentMb: string, totalMb: string): string => localize('azdata.downloadingProgressMb', "Downloading ({0} / {1} MB)", currentMb, totalMb);
export const downloadFinished = localize('azdata.downloadFinished', "Download finished");
export const installingAzdata = localize('azdata.installingAzdata', "Installing azdata...");
export const updatingAzdata = localize('azdata.updatingAzdata', "updating azdata...");
export const installingAzdata = localize('azdata.installingAzdata', "Installing Azure Data CLI...");
export const updatingAzdata = localize('azdata.updatingAzdata', "Updating Azure Data CLI...");
export const azdataInstalled = localize('azdata.azdataInstalled', "Azure Data CLI was successfully installed. Restarting Azure Data Studio is required to complete configuration - features will not be activated until this is done.");
export const azdataUpdated = (version: string) => localize('azdata.azdataUpdated', "Azure Data CLI was successfully updated to version: {0}.", version);
export const yes = localize('azdata.yes', "Yes");
@@ -22,46 +24,45 @@ export const accept = localize('azdata.accept', "Accept");
export const decline = localize('azdata.decline', "Decline");
export const doNotAskAgain = localize('azdata.doNotAskAgain', "Don't Ask Again");
export const askLater = localize('azdata.askLater', "Ask Later");
export const downloadingTo = (name: string, location: string): string => localize('azdata.downloadingTo', "Downloading {0} to {1}", name, location);
export const downloadingTo = (name: string, url: string, location: string): string => localize('azdata.downloadingTo', "Downloading {0} from {1} to {2}", name, url, location);
export const executingCommand = (command: string, args: string[]): string => localize('azdata.executingCommand', "Executing command: '{0} {1}'", command, args?.join(' '));
export const stdoutOutput = (stdout: string): string => localize('azdata.stdoutOutput', "stdout: {0}", stdout);
export const stderrOutput = (stderr: string): string => localize('azdata.stderrOutput', "stderr: {0}", stderr);
export const checkingLatestAzdataVersion = localize('azdata.checkingLatestAzdataVersion', "Checking for latest available version of azdata");
export const checkingLatestAzdataVersion = localize('azdata.checkingLatestAzdataVersion', "Checking for latest available version of Azure Data CLI");
export const gettingTextContentsOfUrl = (url: string): string => localize('azdata.gettingTextContentsOfUrl', "Getting text contents of resource at URL {0}", url);
export const foundAzdataVersionToUpdateTo = (newVersion: string, currentVersion: string): string => localize('azdata.versionForUpdate', "Found version: {0} that Azure Data CLI can be updated to from current version: {1}.", newVersion, currentVersion);
export const latestAzdataVersionAvailable = (version: string): string => localize('azdata.latestAzdataVersionAvailable', "Latest available Azure Data CLI version: {0}.", version);
export const couldNotFindAzdata = (err: any): string => localize('azdata.couldNotFindAzdata', "Could not find azdata. Error: {0}", err.message ?? err);
export const currentlyInstalledVersionIsLatest = (currentVersion: string): string => localize('azdata.currentlyInstalledVersionIsLatest', "Currently installed version of azdata: {0} is same or newer than any other version available", currentVersion);
export const couldNotFindAzdata = (err: any): string => localize('azdata.couldNotFindAzdata', "Could not find Azure Data CLI. Error: {0}", err.message ?? err);
export const currentlyInstalledVersionIsLatest = (currentVersion: string): string => localize('azdata.currentlyInstalledVersionIsLatest', "Currently installed version of Azure Data CLI: {0} is same or newer than any other version available", currentVersion);
export const promptLog = (logEntry: string) => localize('azdata.promptLog', "Prompting the user to accept the following: {0}", logEntry);
export const promptForAzdataInstall = localize('azdata.couldNotFindAzdataWithPrompt', "Could not find azdata, install it now? If not then some features will not be able to function.");
export const promptForAzdataInstall = localize('azdata.couldNotFindAzdataWithPrompt', "Could not find Azure Data CLI, install it now? If not then some features will not be able to function.");
export const promptForAzdataInstallLog = promptLog(promptForAzdataInstall);
export const promptForAzdataUpdate = (version: string): string => localize('azdata.promptForAzdataUpdate', "A new version of Azure Data CLI ( {0} ) is available, do you wish to update to it now?", version);
export const promptForAzdataUpdateLog = (version: string): string => promptLog(promptForAzdataUpdate(version));
export const downloadError = localize('azdata.downloadError', "Error while downloading");
export const installError = (err: any): string => localize('azdata.installError', "Error installing azdata: {0}", err.message ?? err);
export const updateError = (err: any): string => localize('azdata.updateError', "Error updating azdata: {0}", err.message ?? err);
export const installError = (err: any): string => localize('azdata.installError', "Error installing Azure Data CLI: {0}", err.message ?? err);
export const updateError = (err: any): string => localize('azdata.updateError', "Error updating Azure Data CLI: {0}", err.message ?? err);
export const platformUnsupported = (platform: string): string => localize('azdata.platformUnsupported', "Platform '{0}' is currently unsupported", platform);
export const unexpectedCommandError = (errMsg: string): string => localize('azdata.unexpectedCommandError', "Unexpected error executing command: {0}", errMsg);
export const unexpectedExitCode = (code: number, err: string): string => localize('azdata.unexpectedExitCode', "Unexpected exit code from command: {1} ({0})", code, err);
export const noAzdata = localize('azdata.NoAzdata', "No Azure Data CLI is available, [install the Azure Data CLI](command:azdata.install) to enable the features that require it.");
export const noAzdata = localize('azdata.noAzdata', "No Azure Data CLI is available, run the command 'Azure Data CLI: Install' to enable the features that require it.");
export const noAzdataWithLink = localize('azdata.noAzdataWithLink', "No Azure Data CLI is available, [install the Azure Data CLI](command:azdata.install) to enable the features that require it.");
export const skipInstall = (config: string): string => localize('azdata.skipInstall', "Skipping installation of azdata, since the operation was not user requested and config option: {0}.{1} is {2}", azdataConfigSection, azdataInstallKey, config);
export const skipUpdate = (config: string): string => localize('azdata.skipUpdate', "Skipping update of azdata, since the operation was not user requested and config option: {0}.{1} is {2}", azdataConfigSection, azdataUpdateKey, config);
export const skipInstall = (config: string): string => localize('azdata.skipInstall', "Skipping installation of Azure Data CLI, since the operation was not user requested and config option: {0}.{1} is {2}", azdataConfigSection, azdataInstallKey, config);
export const skipUpdate = (config: string): string => localize('azdata.skipUpdate', "Skipping update of Azure Data CLI, since the operation was not user requested and config option: {0}.{1} is {2}", azdataConfigSection, azdataUpdateKey, config);
export const noReleaseVersion = (platform: string, releaseInfo: string): string => localize('azdata.noReleaseVersion', "No release version available for platform '{0}'\nRelease info: ${1}", platform, releaseInfo);
export const noDownloadLink = (platform: string, releaseInfo: string): string => localize('azdata.noDownloadLink', "No download link available for platform '{0}'\nRelease info: ${1}", platform, releaseInfo);
export const failedToParseReleaseInfo = (url: string, fileContents: string, err: any): string => localize('azdata.failedToParseReleaseInfo', "Failed to parse the JSON of contents at: {0}.\nFile contents:\n{1}\nError: {2}", url, fileContents, getErrorMessage(err));
export const azdataUserSettingRead = (configName: string, configValue: string): string => localize('azdata.azdataUserSettingReadLog', "Azure Data CLI user setting: {0}.{1} read, value: {2}", azdataConfigSection, configName, configValue);
export const azdataUserSettingUpdated = (configName: string, configValue: string): string => localize('azdata.azdataUserSettingUpdatedLog', "Azure Data CLI user setting: {0}.{1} updated, newValue: {2}", azdataConfigSection, configName, configValue);
export const userResponseToInstallPrompt = (response: string | undefined): string => localize('azdata.userResponseInstall', "User Response on prompt to install azdata: {0}", response);
export const userResponseToUpdatePrompt = (response: string | undefined): string => localize('azdata.userResponseUpdate', "User Response on prompt to update azdata: {0}", response);
export const userResponseToInstallPrompt = (response: string | undefined): string => localize('azdata.userResponseInstall', "User Response on prompt to install Azure Data CLI: {0}", response);
export const userResponseToUpdatePrompt = (response: string | undefined): string => localize('azdata.userResponseUpdate', "User Response on prompt to update Azure Data CLI: {0}", response);
export const userRequestedInstall = localize('azdata.userRequestedInstall', "User requested to install Azure Data CLI using 'Azure Data CLI: Install' command");
export const userRequestedUpdate = localize('azdata.userRequestedUpdate', "User requested to update Azure Data CLI using 'Azure Data CLI: Check for Update' command");
export const userRequestedAcceptEula = localize('azdata.acceptEula', "User requested to be prompted for accepting EULA by invoking 'Azure Data CLI: Accept EULA' command");
export const updateCheckSkipped = localize('azdata.updateCheckSkipped', "No check for new Azure Data CLI version availability performed as Azure Data CLI was not found to be installed");
export const eulaNotAccepted = localize('azdata.eulaNotAccepted', "Microsoft Privacy statement and Azure Data CLI license terms have not been accepted. Execute the command: [Azure Data CLI: Accept EULA](command:azdata.acceptEula) to accept EULA to enable the features that requires Azure Data CLI.");
export const installManually = (expectedVersion: string, instructionsUrl: string) => localize('azdata.installManually', "Azure Data CLI is not installed. Version: {0} needs to be installed or some features may not work. Please install it manually using these [instructions]({1}). Restart ADS when installation is done.", expectedVersion, instructionsUrl);
export const installCorrectVersionManually = (currentVersion: string, expectedVersion: string, instructionsUrl: string) => localize('azdata.installCorrectVersionManually', "Azure Data CLI version: {0} is installed, version: {1} needs to be installed or some features may not work. Please uninstall the current version and then install the correct version manually using these [instructions]({2}). Restart ADS when installation is done.", currentVersion, expectedVersion, instructionsUrl);
export const promptForEula = (privacyStatementUrl: string, eulaUrl: string) => localize('azdata.promptForEula', "It is required to accept the [Microsoft Privacy Statement]({0}) and the [Azure Data CLI license terms]({1}) to use this extension. Declining this will result in some features not working.", privacyStatementUrl, eulaUrl);
export const promptForEulaLog = (privacyStatementUrl: string, eulaUrl: string) => promptLog(promptForEula(privacyStatementUrl, eulaUrl));
export const userResponseToEulaPrompt = (response: string | undefined) => localize('azdata.promptForEulaResponse', "User response to EULA prompt: {0}", response);
export const eulaAcceptedStateOnStartup = (eulaAccepted: boolean) => localize('azdata.eulaAcceptedStateOnStartup', "'EULA Accepted' state on startup: {0}", eulaAccepted);
export const eulaAcceptedStateUpdated = (eulaAccepted: boolean) => localize('azdata.eulaAcceptedStateUpdated', "Updated 'EULA Accepted' state to: {0}", eulaAccepted);

View File

@@ -3,7 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import * as should from 'should';
import * as sinon from 'sinon';
import * as vscode from 'vscode';
@@ -11,10 +10,14 @@ import * as azdata from '../azdata';
import * as childProcess from '../common/childProcess';
import { HttpClient } from '../common/httpClient';
import * as utils from '../common/utils';
import * as constants from '../constants';
import * as loc from '../localizedConstants';
const oldAzdataMock = new azdata.AzdataTool('/path/to/azdata', '0.0.0');
/**
* This matches the schema of the JSON file used to determine the current version of
* azdata - do not modify unless also updating the corresponding JSON file
*/
const releaseJson = {
win32: {
'version': '9999.999.999',
@@ -27,7 +30,7 @@ const releaseJson = {
'version': '9999.999.999'
}
};
let executeSudoCommandStub: sinon.SinonStub;
describe('azdata', function () {
afterEach(function (): void {
@@ -55,9 +58,11 @@ describe('azdata', function () {
});
describe('installAzdata', function (): void {
beforeEach(function (): void {
sinon.stub(vscode.window, 'showErrorMessage').returns(Promise.resolve(<any>loc.yes));
sinon.stub(utils, 'searchForCmd').returns(Promise.resolve('/path/to/azdata'));
executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand').returns(Promise.resolve({ stdout: '', stderr: '' }));
});
it('successful install', async function (): Promise<void> {
@@ -77,8 +82,12 @@ describe('azdata', function () {
if (process.platform === 'win32') {
it('unsuccessful download - win32', async function (): Promise<void> {
sinon.stub(HttpClient, 'downloadFile').rejects();
const downloadPromise = azdata.checkAndInstallAzdata();
await should(downloadPromise).be.rejected();
sinon.stub(childProcess, 'executeCommand')
.onFirstCall()
.rejects(new Error('not Found')) // First call mock the tool not being found
.resolves({ stdout: '1.0.0', stderr: '' });
const azdataTool = await azdata.checkAndInstallAzdata();
should(azdataTool).be.undefined();
});
}
@@ -100,6 +109,7 @@ describe('azdata', function () {
describe('updateAzdata', function (): void {
beforeEach(function (): void {
sinon.stub(vscode.window, 'showInformationMessage').returns(Promise.resolve(<any>loc.yes));
executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand').returns(Promise.resolve({ stdout: '', stderr: '' }));
});
it('successful update', async function (): Promise<void> {
@@ -107,7 +117,6 @@ describe('azdata', function () {
case 'win32':
await testWin32SuccessfulUpdate();
break;
case 'darwin':
await testDarwinSuccessfulUpdate();
break;
@@ -132,9 +141,8 @@ describe('azdata', function () {
});
describe('discoverLatestAvailableAzdataVersion', function (): void {
this.timeout(20000);
it(`finds latest available version of azdata successfully`, async function (): Promise<void> {
// if the latest version is not discovered then the following call throws failing the test
it('finds latest available version of azdata successfully', async function (): Promise<void> {
sinon.stub(HttpClient, 'getTextContent').resolves(JSON.stringify(releaseJson));
await azdata.discoverLatestAvailableAzdataVersion();
});
});
@@ -142,7 +150,7 @@ describe('azdata', function () {
});
async function testLinuxUnsuccessfulUpdate() {
const executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand').rejects();
executeSudoCommandStub.rejects();
const updateDone = await azdata.checkAndUpdateAzdata(oldAzdataMock);
should(updateDone).be.false();
should(executeSudoCommandStub.calledOnce).be.true();
@@ -172,7 +180,7 @@ async function testDarwinUnsuccessfulUpdate() {
return Promise.reject(new Error('not Found'));
})
.callsFake(async (_command: string, _args: string[]) => { // by default return success
return Promise.resolve({stderr: '', stdout: 'success'});
return Promise.resolve({ stderr: '', stdout: 'success' });
});
const updateDone = await azdata.checkAndUpdateAzdata(oldAzdataMock);
should(updateDone).be.false();
@@ -181,16 +189,16 @@ async function testDarwinUnsuccessfulUpdate() {
async function testWin32UnsuccessfulUpdate() {
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
const executeCommandStub = sinon.stub(childProcess, 'executeCommand').rejects();
executeSudoCommandStub.rejects();
const updateDone = await azdata.checkAndUpdateAzdata(oldAzdataMock);
should(updateDone).be.false();
should(executeCommandStub.calledOnce).be.true();
should(updateDone).be.false('Update should not have been successful');
should(executeSudoCommandStub.calledOnce).be.true();
}
async function testLinuxSuccessfulUpdate() {
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
const executeCommandStub = sinon.stub(childProcess, 'executeCommand').returns(Promise.resolve({ stdout: '0.0.0', stderr: '' }));
const executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand').returns(Promise.resolve({ stdout: '0.0.0', stderr: '' }));
executeSudoCommandStub.resolves({ stdout: '0.0.0', stderr: '' });
await azdata.checkAndUpdateAzdata(oldAzdataMock);
should(executeSudoCommandStub.callCount).be.equal(6);
should(executeCommandStub.calledOnce).be.true();
@@ -209,51 +217,39 @@ async function testDarwinSuccessfulUpdate() {
}];
const executeCommandStub = sinon.stub(childProcess, 'executeCommand')
.onThirdCall() //third call is brew info azdata-cli --json which needs to return json of new available azdata versions.
.callsFake(async (command: string, args: string[]) => {
should(command).be.equal('brew');
should(args).deepEqual(['info', 'azdata-cli', '--json']);
return Promise.resolve({
.resolves({
stderr: '',
stdout: JSON.stringify(brewInfoOutput)
});
})
.callsFake(async (_command: string, _args: string[]) => { // return success on all other command executions
return Promise.resolve({ stdout: '0.0.0', stderr: '' });
});
.resolves({ stdout: '0.0.0', stderr: '' });
await azdata.checkAndUpdateAzdata(oldAzdataMock);
should(executeCommandStub.callCount).be.equal(6);
should(executeCommandStub.getCall(2).args[0]).be.equal('brew', '3rd call should have been to brew');
should(executeCommandStub.getCall(2).args[1]).deepEqual(['info', 'azdata-cli', '--json'], '3rd call did not have expected arguments');
}
async function testWin32SuccessfulUpdate() {
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
const executeCommandStub = sinon.stub(childProcess, 'executeCommand').callsFake(async (command: string, args: string[]) => {
should(command).be.equal('msiexec');
should(args[0]).be.equal('/qn');
should(args[1]).be.equal('/i');
should(path.basename(args[2])).be.equal(constants.azdataUri);
return { stdout: '0.0.0', stderr: '' };
});
await azdata.checkAndUpdateAzdata(oldAzdataMock);
should(executeCommandStub.calledOnce).be.true();
should(executeSudoCommandStub.calledOnce).be.true('executeSudoCommand should have been called once');
should(executeSudoCommandStub.getCall(0).args[0]).startWith('msiexec /qn /i');
}
async function testWin32SuccessfulInstall() {
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
const executeCommandStub = sinon.stub(childProcess, 'executeCommand')
.onFirstCall()
.callsFake(async (_command: string, _args: string[]) => {
return Promise.reject(new Error('not Found'));
})
.callsFake(async (command: string, args: string[]) => {
should(command).be.equal('msiexec');
should(args[0]).be.equal('/qn');
should(args[1]).be.equal('/i');
should(path.basename(args[2])).be.equal(constants.azdataUri);
return { stdout: '0.0.0', stderr: '' };
});
.rejects(new Error('not Found')) // First call mock the tool not being found
.resolves({ stdout: '1.0.0', stderr: '' });
executeSudoCommandStub
.returns({ stdout: '', stderr: '' });
await azdata.checkAndInstallAzdata();
should(executeCommandStub.calledTwice).be.true();
should(executeCommandStub.calledTwice).be.true(`executeCommand should have been called twice. Actual ${executeCommandStub.getCalls().length}`);
should(executeSudoCommandStub.calledOnce).be.true(`executeSudoCommand should have been called once. Actual ${executeSudoCommandStub.getCalls().length}`);
should(executeSudoCommandStub.getCall(0).args[0]).startWith('msiexec /qn /i');
}
async function testDarwinSuccessfulInstall() {
@@ -272,23 +268,17 @@ async function testDarwinSuccessfulInstall() {
async function testLinuxSuccessfulInstall() {
const executeCommandStub = sinon.stub(childProcess, 'executeCommand')
.onFirstCall()
.callsFake(async (_command: string, _args: string[]) => {
return Promise.reject(new Error('not Found'));
})
.callsFake(async (_command: string, _args: string[]) => {
return Promise.resolve({ stdout: '0.0.0', stderr: '' });
});
const executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand')
.callsFake(async (_command: string ) => {
return Promise.resolve({ stdout: 'success', stderr: '' });
});
.rejects(new Error('not Found'))
.resolves({ stdout: '0.0.0', stderr: '' });
executeSudoCommandStub
.resolves({ stdout: 'success', stderr: '' });
await azdata.checkAndInstallAzdata();
should(executeSudoCommandStub.callCount).be.equal(6);
should(executeCommandStub.calledThrice).be.true();
}
async function testLinuxUnsuccessfulInstall() {
const executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand').rejects();
executeSudoCommandStub.rejects();
const downloadPromise = azdata.installAzdata();
await should(downloadPromise).be.rejected();
should(executeSudoCommandStub.calledOnce).be.true();
@@ -302,9 +292,9 @@ async function testDarwinUnsuccessfulInstall() {
}
async function testWin32UnsuccessfulInstall() {
const executeCommandStub = sinon.stub(childProcess, 'executeCommand').rejects();
executeSudoCommandStub.rejects();
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
const downloadPromise = azdata.installAzdata();
await should(downloadPromise).be.rejected();
should(executeCommandStub.calledOnce).be.true();
should(executeSudoCommandStub.calledOnce).be.true();
}

View File

@@ -73,12 +73,12 @@ describe('HttpClient', function (): void {
});
describe('getTextContent', function (): void {
it.skip('Gets file contents correctly', async function (): Promise<void> {
it('Gets file contents correctly', async function (): Promise<void> {
nock('https://127.0.0.1')
.get('/arbitraryFile')
.replyWithFile(200, __filename);
const receivedContents = await HttpClient.getTextContent(`https://127.0.0.1/arbitraryFile`);
should(receivedContents).equal(await fs.promises.readFile(__filename));
should(receivedContents).equal((await fs.promises.readFile(__filename)).toString());
});
it('rejects on response error', async function (): Promise<void> {

View File

@@ -143,25 +143,13 @@ declare module 'azdata-ext' {
},
status: {
readyReplicas: string, // "1/1"
state: string, // "Ready"
state: string, // "Ready",
logSearchDashboard: string, // https://127.0.0.1:30777/kibana/app/kibana#/discover?_a=(query:(language:kuery,query:'custom_resource_name:miaa1'))
metricsDashboard: string, // https://127.0.0.1:30777/grafana/d/40q72HnGk/sql-managed-instance-metrics?var-hostname=miaa1-0
externalEndpoint?: string // "10.91.86.39:32718"
}
}
export interface PostgresServerShowResult {
apiVersion: string, // "arcdata.microsoft.com/v1alpha1"
kind: string, // "postgresql-12"
metadata: {
creationTimestamp: string, // "2020-08-19T20:25:11Z"
generation: number, // 1
name: string, // "chgagnon-pg"
namespace: string, // "arc",
resourceVersion: string, // "214944",
selfLink: string, // "/apis/arcdata.microsoft.com/v1alpha1/namespaces/arc/postgresql-12s/chgagnon-pg",
uid: string, // "26d0f5bb-0c0b-4225-a6b5-5be2bf6feac0"
}
}
export interface PostgresServerShowResult {
apiVersion: string, // "arcdata.microsoft.com/v1alpha1"
kind: string, // "postgresql-12"
@@ -175,25 +163,56 @@ declare module 'azdata-ext' {
uid: string, // "26d0f5bb-0c0b-4225-a6b5-5be2bf6feac0"
},
spec: {
backups: {
deltaMinutes: number, // 3,
fullMinutes: number, // 10,
tiers: [
{
retention: {
maximums: string[], // [ "6", "512MB" ],
minimums: string[], // [ "3" ]
engine: {
extensions: {
name: string // "citus"
}[],
settings: {
default: { [key: string]: string } // { "max_connections": "101", "work_mem": "4MB" }
}
},
scale: {
shards: number // 1
},
scheduling: {
default: {
resources: {
requests: {
cpu: string, // "1.5"
memory: string // "256Mi"
},
limits: {
cpu: string, // "1.5"
memory: string // "256Mi"
}
}
}
},
service: {
type: string, // "NodePort"
port: number // 5432
},
storage: {
volumeSize: string, // "1Gi"
data: {
className: string, // "local-storage"
size: string // "5Gi"
},
logs: {
className: string, // "local-storage"
size: string // "5Gi"
},
backups: {
className: string, // "local-storage"
size: string // "5Gi"
}
}
]
},
status: {
externalEndpoint: string, // "10.130.12.136:26630"
readyPods: string, // "1/1",
state: string // "Ready"
}
state: string, // "Ready"
logSearchDashboard: string, // https://127.0.0.1:30777/kibana/app/kibana#/discover?_a=(query:(language:kuery,query:'custom_resource_name:pg1'))
metricsDashboard: string, // https://127.0.0.1:30777/grafana/d/40q72HnGk/sql-managed-instance-metrics?var-hostname=pg1
}
}
@@ -219,8 +238,25 @@ declare module 'azdata-ext' {
},
postgres: {
server: {
delete(name: string): Promise<AzdataOutput<void>>,
list(): Promise<AzdataOutput<PostgresServerListResult[]>>,
show(name: string): Promise<AzdataOutput<PostgresServerShowResult>>
show(name: string): Promise<AzdataOutput<PostgresServerShowResult>>,
edit(
name: string,
args: {
adminPassword?: boolean,
coresLimit?: string,
coresRequest?: string,
engineSettings?: string,
extensions?: string,
memoryLimit?: string,
memoryRequest?: string,
noWait?: boolean,
port?: number,
replaceEngineSettings?: boolean,
workers?: number
},
additionalEnvVars?: { [key: string]: string }): Promise<AzdataOutput<void>>
}
},
sql: {