diff --git a/extensions/arc/README.md b/extensions/arc/README.md
index eea2df9518..ec1fb957eb 100644
--- a/extensions/arc/README.md
+++ b/extensions/arc/README.md
@@ -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
diff --git a/extensions/arc/notebooks/arcDataServices/_data/toc.yml b/extensions/arc/notebooks/arcDataServices/_data/toc.yml
index 08aa620181..8648d839fb 100644
--- a/extensions/arc/notebooks/arcDataServices/_data/toc.yml
+++ b/extensions/arc/notebooks/arcDataServices/_data/toc.yml
@@ -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
diff --git a/extensions/arc/notebooks/arcDataServices/content/postgres/readme.md b/extensions/arc/notebooks/arcDataServices/content/postgres/readme.md
index a3fe0eee38..f374526c4b 100644
--- a/extensions/arc/notebooks/arcDataServices/content/postgres/readme.md
+++ b/extensions/arc/notebooks/arcDataServices/content/postgres/readme.md
@@ -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)
diff --git a/extensions/arc/notebooks/arcDataServices/content/postgres/toc.yml b/extensions/arc/notebooks/arcDataServices/content/postgres/toc.yml
index cf63274703..71298e6fc7 100644
--- a/extensions/arc/notebooks/arcDataServices/content/postgres/toc.yml
+++ b/extensions/arc/notebooks/arcDataServices/content/postgres/toc.yml
@@ -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
diff --git a/extensions/arc/notebooks/arcDataServices/content/postgres/tsg100-troubleshoot-postgres.ipynb b/extensions/arc/notebooks/arcDataServices/content/postgres/tsg100-troubleshoot-postgres.ipynb
index a0ac270ea8..8a7c7af4fb 100644
--- a/extensions/arc/notebooks/arcDataServices/content/postgres/tsg100-troubleshoot-postgres.ipynb
+++ b/extensions/arc/notebooks/arcDataServices/content/postgres/tsg100-troubleshoot-postgres.ipynb
@@ -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}')"
]
},
{
diff --git a/extensions/arc/notebooks/arcDeployment/deploy.arc.control.plane.ipynb b/extensions/arc/notebooks/arcDeployment/deploy.arc.data.controller.ipynb
similarity index 86%
rename from extensions/arc/notebooks/arcDeployment/deploy.arc.control.plane.ipynb
rename to extensions/arc/notebooks/arcDeployment/deploy.arc.data.controller.ipynb
index e24b404cea..a87aac9123 100644
--- a/extensions/arc/notebooks/arcDeployment/deploy.arc.control.plane.ipynb
+++ b/extensions/arc/notebooks/arcDeployment/deploy.arc.data.controller.ipynb
@@ -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"
@@ -247,4 +243,4 @@
"execution_count": null
}
]
-}
+}
\ No newline at end of file
diff --git a/extensions/arc/notebooks/arcDeployment/deploy.postgres.existing.arc.ipynb b/extensions/arc/notebooks/arcDeployment/deploy.postgres.existing.arc.ipynb
index 37ae3fabe1..1a31f4a91d 100644
--- a/extensions/arc/notebooks/arcDeployment/deploy.postgres.existing.arc.ipynb
+++ b/extensions/arc/notebooks/arcDeployment/deploy.postgres.existing.arc.ipynb
@@ -25,12 +25,12 @@
"source": [
"\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",
"Please press the \"Run All\" button to run the notebook"
],
@@ -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": {
diff --git a/extensions/arc/notebooks/arcDeployment/deploy.sql.existing.arc.ipynb b/extensions/arc/notebooks/arcDeployment/deploy.sql.existing.arc.ipynb
index b19b8af493..c05627d0ef 100644
--- a/extensions/arc/notebooks/arcDeployment/deploy.sql.existing.arc.ipynb
+++ b/extensions/arc/notebooks/arcDeployment/deploy.sql.existing.arc.ipynb
@@ -25,12 +25,12 @@
"source": [
"\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 Azure SQL managed instance on an existing Azure Arc data cluster.\n",
+ "This notebook walks through the process of creating a SQL managed instance - 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",
"Please press the \"Run All\" button to run the notebook"
],
@@ -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": {
diff --git a/extensions/arc/package.json b/extensions/arc/package.json
index c4594cbb4c..3eb07f4a9e 100644
--- a/extensions/arc/package.json
+++ b/extensions/arc/package.json
@@ -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"
}
}
diff --git a/extensions/arc/src/common/utils.ts b/extensions/arc/src/common/utils.ts
index dbc75909bf..f86c864342 100644
--- a/extensions/arc/src/common/utils.ts
+++ b/extensions/arc/src/common/utils.ts
@@ -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 {
- const title = loc.resourceDeletionWarning(name);
+export async function promptForInstanceDeletion(name: string): Promise {
+ 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 :
* @param address The address to parse
diff --git a/extensions/arc/src/constants.ts b/extensions/arc/src/constants.ts
index 974138eaea..7522f9373a 100644
--- a/extensions/arc/src/constants.ts
+++ b/extensions/arc/src/constants.ts
@@ -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;
diff --git a/extensions/arc/src/extension.ts b/extensions/arc/src/extension.ts
index a1248aa03f..fccd30eb2f 100644
--- a/extensions/arc/src/extension.ts
+++ b/extensions/arc/src/extension.ts
@@ -28,6 +28,14 @@ export async function activate(context: vscode.ExtensionContext): Promise {
+ 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();
diff --git a/extensions/arc/src/localizedConstants.ts b/extensions/arc/src/localizedConstants.ts
index bf458a6fa0..699309377e 100644
--- a/extensions/arc/src/localizedConstants.ts
+++ b/extensions/arc/src/localizedConstants.ts
@@ -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)); }
diff --git a/extensions/arc/src/models/controllerModel.ts b/extensions/arc/src/models/controllerModel.ts
index 34441f6a1f..3ad0a04228 100644
--- a/extensions/arc/src/models/controllerModel.ts
+++ b/extensions/arc/src/models/controllerModel.ts
@@ -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 {
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
*/
diff --git a/extensions/arc/src/models/miaaModel.ts b/extensions/arc/src/models/miaaModel.ts
index f4383d0aef..46c5c8c852 100644
--- a/extensions/arc/src/models/miaaModel.ts
+++ b/extensions/arc/src/models/miaaModel.ts
@@ -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 | 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 = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
}
@@ -155,83 +157,58 @@ export class MiaaModel extends ResourceModel {
if (this._connectionProfile) {
return;
}
- let connection: azdata.connection.ConnectionProfile | azdata.connection.Connection | undefined;
+ const ipAndPort = parseIpAndPort(this.config?.status.externalEndpoint || '');
+ let connectionProfile: azdata.IConnectionProfile | undefined = {
+ serverName: `${ipAndPort.ip},${ipAndPort.port}`,
+ databaseName: '',
+ authenticationType: 'SqlLogin',
+ providerName: 'MSSQL',
+ connectionName: '',
+ userName: this._miaaInfo.userName || '',
+ password: '',
+ savePassword: true,
+ groupFullName: undefined,
+ saveProfile: true,
+ id: '',
+ groupId: undefined,
+ options: {}
+ };
+
+ // If we have the ID stored then try to retrieve the password from previous connections
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);
+ 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) {
- // ignore - the connection may not necessarily exist anymore and in that case we'll just reprompt for a connection
+ console.warn(`Unexpected error fetching password for MIAA instance ${err}`);
+ // ignore - something happened fetching the password so just reprompt
}
}
- 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: '',
- databaseName: '',
- authenticationType: 'SqlLogin',
- providerName: 'MSSQL',
- connectionName: '',
- userName: 'sa',
- password: '',
- savePassword: true,
- groupFullName: undefined,
- saveProfile: true,
- id: '',
- 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 (!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 (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) {
+ this.updateConnectionProfile(connectionProfile);
} else {
throw new UserCancelledError();
}
@@ -240,6 +217,7 @@ export class MiaaModel extends ResourceModel {
private async updateConnectionProfile(connectionProfile: azdata.IConnectionProfile): Promise {
this._connectionProfile = connectionProfile;
this.info.connectionId = connectionProfile.id;
+ this._miaaInfo.userName = connectionProfile.userName;
await this._treeDataProvider.saveControllers();
}
}
diff --git a/extensions/arc/src/models/postgresModel.ts b/extensions/arc/src/models/postgresModel.ts
index 4c051a629f..09931200c5 100644
--- a/extensions/arc/src/models/postgresModel.ts
+++ b/extensions/arc/src/models/postgresModel.ts
@@ -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;
- 'containerStatuses'?: Array;
- 'ephemeralContainerStatuses'?: any[]; // Array;
- 'hostIP'?: string;
- 'initContainerStatuses'?: any[]; // Array;
- 'message'?: string;
- 'nominatedNodeName'?: string;
- 'phase'?: string;
- 'podIP'?: string;
- 'podIPs'?: any[]; // Array;
- '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();
- private readonly _onPodsUpdated = new vscode.EventEmitter();
- 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();
+ public onConfigUpdated = this._onConfigUpdated.event;
+ public configLastUpdated?: Date;
+
+ constructor(private _controllerModel: ControllerModel, info: ResourceInfo, registration: Registration) {
super(info, registration);
+ this._azdataApi = 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 {
- return 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 {
- return 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 {
- return 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}`);
- }
+ 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);
}
}
diff --git a/extensions/arc/src/providers/arcControllersOptionsSourceProvider.ts b/extensions/arc/src/providers/arcControllersOptionsSourceProvider.ts
index ac1089c09d..2623ab60fa 100644
--- a/extensions/arc/src/providers/arcControllersOptionsSourceProvider.ts
+++ b/extensions/arc/src/providers/arcControllersOptionsSourceProvider.ts
@@ -41,7 +41,9 @@ export class ArcControllersOptionsSourceProvider implements rd.IOptionsSourcePro
}
getVariableValue(variableName: string, controllerLabel: string): Promise {
- 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 {
diff --git a/extensions/arc/src/test/common/utils.test.ts b/extensions/arc/src/test/common/utils.test.ts
index fde0f65ac2..207f14c96d 100644
--- a/extensions/arc/src/test/common/utils.test.ts
+++ b/extensions/arc/src/test/common/utils.test.ts
@@ -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 {
- 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';
diff --git a/extensions/arc/src/test/mocks/fakeAzdataApi.ts b/extensions/arc/src/test/mocks/fakeAzdataApi.ts
new file mode 100644
index 0000000000..7312e948f5
--- /dev/null
+++ b/extensions/arc/src/test/mocks/fakeAzdataApi.ts
@@ -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> { throw new Error('Method not implemented.'); },
+ endpoint: {
+ async list(): Promise> { return { result: [] }; }
+ },
+ config: {
+ list(): Promise> { throw new Error('Method not implemented.'); },
+ async show(): Promise> { return { result: undefined! }; }
+ }
+ },
+ postgres: {
+ server: {
+ delete(_name: string): Promise> { throw new Error('Method not implemented.'); },
+ async list(): Promise> { return { result: self.postgresInstances }; },
+ show(_name: string): Promise> { 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> { throw new Error('Method not implemented.'); }
+ }
+ },
+ sql: {
+ mi: {
+ delete(_name: string): Promise> { throw new Error('Method not implemented.'); },
+ async list(): Promise> { return { result: self.miaaInstances }; },
+ show(_name: string): Promise> { throw new Error('Method not implemented.'); }
+ }
+ }
+ };
+ }
+ getPath(): Promise {
+ throw new Error('Method not implemented.');
+ }
+ login(_endpoint: string, _username: string, _password: string): Promise> {
+ return undefined;
+ }
+ version(): Promise> {
+ throw new Error('Method not implemented.');
+ }
+ getSemVersion(): any {
+ throw new Error('Method not implemented.');
+ }
+
+}
diff --git a/extensions/arc/src/test/ui/tree/azureArcTreeDataProvider.test.ts b/extensions/arc/src/test/ui/tree/azureArcTreeDataProvider.test.ts
index a6161223f0..3d429de41a 100644
--- a/extensions/arc/src/test/ui/tree/azureArcTreeDataProvider.test.ts
+++ b/extensions/arc/src/test/ui/tree/azureArcTreeDataProvider.test.ts
@@ -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 {
+ const mockArcExtension = TypeMoq.Mock.ofType>();
+ const mockArcApi = TypeMoq.Mock.ofType();
+ 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 {
+ 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 {
+ 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 {
+ 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');
+ });
+ });
});
diff --git a/extensions/arc/src/typings/arc.d.ts b/extensions/arc/src/typings/arc.d.ts
index 8784c2ac54..0fcc14c352 100644
--- a/extensions/arc/src/typings/arc.d.ts
+++ b/extensions/arc/src/typings/arc.d.ts
@@ -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,
diff --git a/extensions/arc/src/ui/dashboards/controller/controllerDashboardOverviewPage.ts b/extensions/arc/src/ui/dashboards/controller/controllerDashboardOverviewPage.ts
index 13d53c3d81..a917e26adc 100644
--- a/extensions/arc/src/ui/dashboards/controller/controllerDashboardOverviewPage.ts
+++ b/extensions/arc/src/ui/dashboards/controller/controllerDashboardOverviewPage.ts
@@ -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({
+ 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({
- value: r.instanceName || '',
- CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
- }).component();
- } else {
- nameComponent = this.modelView.modelBuilder.hyperlink()
- .withProperties({
- label: r.instanceName || '',
- url: ''
- }).component();
- (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) */];
+ const nameComponent = this.modelView.modelBuilder.hyperlink()
+ .withProperties({
+ label: r.instanceName || '',
+ url: ''
+ }).component();
+
+ 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;
}
diff --git a/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts b/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts
index 0beddaeb1d..4bf3ad19ae 100644
--- a/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts
+++ b/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts
@@ -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({
+ 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;
diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresBackupPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresBackupPage.ts
deleted file mode 100644
index e0cccd65df..0000000000
--- a/extensions/arc/src/ui/dashboards/postgres/postgresBackupPage.ts
+++ /dev/null
@@ -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({ value: loc.backup }).component();
- }
-
- protected get toolbarContainer(): azdata.ToolbarContainer {
- return this.modelView.modelBuilder.toolbarContainer().component();
- }
-}
diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresComputeStoragePage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresComputeStoragePage.ts
deleted file mode 100644
index f01148cb3d..0000000000
--- a/extensions/arc/src/ui/dashboards/postgres/postgresComputeStoragePage.ts
+++ /dev/null
@@ -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({ value: loc.computeAndStorage }).component();
- }
-
- protected get toolbarContainer(): azdata.ToolbarContainer {
- return this.modelView.modelBuilder.toolbarContainer().component();
- }
-}
diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts
index 50857543eb..183fe123bb 100644
--- a/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts
+++ b/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts
@@ -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({
- 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({
- 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;
}
}
diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts b/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts
index 3c256e3d15..b63d6218ce 100644
--- a/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts
+++ b/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts
@@ -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 {
@@ -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
]
diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts
index bb884d3b2b..cc94746b9b 100644
--- a/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts
+++ b/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts
@@ -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');
}));
diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresNetworkingPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresNetworkingPage.ts
deleted file mode 100644
index ae91757a42..0000000000
--- a/extensions/arc/src/ui/dashboards/postgres/postgresNetworkingPage.ts
+++ /dev/null
@@ -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({ value: loc.networking }).component();
- }
-
- protected get toolbarContainer(): azdata.ToolbarContainer {
- return this.modelView.modelBuilder.toolbarContainer().component();
- }
-}
diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts
index a3d501cc73..d293640215 100644
--- a/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts
+++ b/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts
@@ -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({
- 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({
- label: this.getKibanaLink(),
- url: this.getKibanaLink()
- }).component();
+ this.kibanaLink = this.modelView.modelBuilder.hyperlink().component();
- this.grafanaLink = this.modelView.modelBuilder.hyperlink()
- .withProperties({
- label: this.getGrafanaLink(),
- url: this.getGrafanaLink()
- }).component();
+ this.grafanaLink = this.modelView.modelBuilder.hyperlink().component();
this.kibanaLoading = this.modelView.modelBuilder.loadingComponent()
- .withItem(this.kibanaLink)
- .withProperties({
- loading: !this._controllerModel.endpointsLastUpdated
- }).component();
+ .withProperties(
+ { loading: !this._postgresModel?.configLastUpdated }
+ )
+ .component();
this.grafanaLoading = this.modelView.modelBuilder.loadingComponent()
- .withItem(this.grafanaLink)
- .withProperties({
- loading: !this._controllerModel.endpointsLastUpdated
- }).component();
+ .withProperties(
+ { loading: !this._postgresModel?.configLastUpdated }
+ )
+ .component();
+
+ this.refreshDashboardLinks();
+
+ this.kibanaLoading.component = this.kibanaLink;
+ this.grafanaLoading.component = this.grafanaLink;
const endpointsTable = this.modelView.modelBuilder.declarativeTable().withProperties({
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({
- value: loc.serverGroupNodes,
- CSSStyles: titleCSS
- }).component());
-
- this.nodesTable = this.modelView.modelBuilder.declarativeTable().withProperties({
- 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({
- 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;
- }
-
- 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;
+ const grafanaUrl = this._postgresModel.config.status.metricsDashboard ?? '';
+ this.grafanaLink.label = grafanaUrl;
+ this.grafanaLink.url = grafanaUrl;
+ 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();
}
}
diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts
index 393e9b129a..03fb1e7858 100644
--- a/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts
+++ b/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts
@@ -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({
- 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() {
diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresResourceHealthPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresResourceHealthPage.ts
deleted file mode 100644
index 2ea339f92b..0000000000
--- a/extensions/arc/src/ui/dashboards/postgres/postgresResourceHealthPage.ts
+++ /dev/null
@@ -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({
- value: loc.resourceHealth,
- CSSStyles: { ...cssStyles.title, 'margin-bottom': '30px' }
- }).component());
-
- content.addItem(this.modelView.modelBuilder.text().withProperties({
- value: loc.podOverview,
- CSSStyles: { ...cssStyles.title, 'margin-block-end': '0' }
- }).component());
-
- this.podsUpdated = this.modelView.modelBuilder.text().withProperties({
- 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({
- 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({
- loading: !this._postgresModel.serviceLastUpdated
- }).component();
-
- content.addItem(this.podsLoading, { CSSStyles: { 'margin-bottom': '30px' } });
-
- // Conditions table
- this.conditionsTable = this.modelView.modelBuilder.declarativeTable().withProperties({
- 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({
- 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({
- 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({
- 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;
- }
-}
diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresSupportRequestPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresSupportRequestPage.ts
index 14a5be718a..34b477daff 100644
--- a/extensions/arc/src/ui/dashboards/postgres/postgresSupportRequestPage.ts
+++ b/extensions/arc/src/ui/dashboards/postgres/postgresSupportRequestPage.ts
@@ -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);
diff --git a/extensions/arc/src/ui/dialogs/connectControllerDialog.ts b/extensions/arc/src/ui/dialogs/connectControllerDialog.ts
index 8fad5ee42f..2b65375b13 100644
--- a/extensions/arc/src/ui/dialogs/connectControllerDialog.ts
+++ b/extensions/arc/src/ui/dialogs/connectControllerDialog.ts
@@ -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();
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 {
diff --git a/extensions/arc/src/ui/dialogs/connectSqlDialog.ts b/extensions/arc/src/ui/dialogs/connectSqlDialog.ts
new file mode 100644
index 0000000000..de9fdd1683
--- /dev/null
+++ b/extensions/arc/src/ui/dialogs/connectSqlDialog.ts
@@ -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();
+
+ 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({
+ value: connectionProfile?.serverName,
+ enabled: false
+ }).component();
+ this.usernameInputBox = this.modelBuilder.inputBox()
+ .withProperties({
+ value: connectionProfile?.userName
+ }).component();
+ this.passwordInputBox = this.modelBuilder.inputBox()
+ .withProperties({
+ inputType: 'password',
+ value: connectionProfile?.password
+ })
+ .component();
+ this.rememberPwCheckBox = this.modelBuilder.checkBox()
+ .withProperties({
+ 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 {
+ 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 {
+ return this._completionPromise.promise;
+ }
+}
diff --git a/extensions/arc/src/ui/tree/azureArcTreeDataProvider.ts b/extensions/arc/src/ui/tree/azureArcTreeDataProvider.ts
index cd92471c70..35c7a85a8e 100644
--- a/extensions/arc/src/ui/tree/azureArcTreeDataProvider.ts
+++ b/extensions/arc/src/ui/tree/azureArcTreeDataProvider.ts
@@ -135,10 +135,14 @@ export class AzureArcTreeDataProvider implements vscode.TreeDataProvider[] = [];
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 {
@@ -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 | 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[] = [];
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;
diff --git a/extensions/arc/src/ui/tree/miaaTreeNode.ts b/extensions/arc/src/ui/tree/miaaTreeNode.ts
index 4e8556874e..cd6509b46c 100644
--- a/extensions/arc/src/ui/tree/miaaTreeNode.ts
+++ b/extensions/arc/src/ui/tree/miaaTreeNode.ts
@@ -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 {
- 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 {
diff --git a/extensions/arc/src/ui/tree/noInstancesTreeNode.ts b/extensions/arc/src/ui/tree/noInstancesTreeNode.ts
new file mode 100644
index 0000000000..0af884108b
--- /dev/null
+++ b/extensions/arc/src/ui/tree/noInstancesTreeNode.ts
@@ -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, '');
+ }
+}
diff --git a/extensions/arc/src/ui/tree/postgresTreeNode.ts b/extensions/arc/src/ui/tree/postgresTreeNode.ts
index 42c8885c06..2d3dcb5633 100644
--- a/extensions/arc/src/ui/tree/postgresTreeNode.ts
+++ b/extensions/arc/src/ui/tree/postgresTreeNode.ts
@@ -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 {
- 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 {
- const postgresDashboard = new PostgresDashboard(this._context, this._controllerModel, this._model);
+ const postgresDashboard = new PostgresDashboard(this._context, this._controllerModel, this.model);
await postgresDashboard.showDashboard();
}
}
diff --git a/extensions/arc/src/ui/tree/refreshTreeNode.ts b/extensions/arc/src/ui/tree/refreshTreeNode.ts
index 7fee0d54f1..7bffa9a512 100644
--- a/extensions/arc/src/ui/tree/refreshTreeNode.ts
+++ b/extensions/arc/src/ui/tree/refreshTreeNode.ts
@@ -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 = {
diff --git a/extensions/arc/src/ui/tree/resourceTreeNode.ts b/extensions/arc/src/ui/tree/resourceTreeNode.ts
index 834dbd6467..b916bafe69 100644
--- a/extensions/arc/src/ui/tree/resourceTreeNode.ts
+++ b/extensions/arc/src/ui/tree/resourceTreeNode.ts
@@ -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 extends TreeNode {
+ constructor(label: string, collapsibleState: vscode.TreeItemCollapsibleState, resourceType: string, public model: M) {
super(label, collapsibleState, resourceType);
}
}
diff --git a/extensions/azdata/README.md b/extensions/azdata/README.md
index cdb5ee3e29..fadee9b62c 100644
--- a/extensions/azdata/README.md
+++ b/extensions/azdata/README.md
@@ -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
diff --git a/extensions/azdata/images/extension.png b/extensions/azdata/images/extension.png
index c86d6d1e00..7a041a392d 100644
Binary files a/extensions/azdata/images/extension.png and b/extensions/azdata/images/extension.png differ
diff --git a/extensions/azdata/package.json b/extensions/azdata/package.json
index 5ef3b7ece0..e046391cde 100644
--- a/extensions/azdata/package.json
+++ b/extensions/azdata/package.json
@@ -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"
}
}
diff --git a/extensions/azdata/src/api.ts b/extensions/azdata/src/api.ts
index ec32f0978b..70b3d0fe4f 100644
--- a/extensions/azdata/src/api.ts
+++ b/extensions/azdata/src/api.ts
@@ -72,6 +72,11 @@ export function getAzdataApi(localAzdataDiscovered: Promise {
+ 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 {
+ await localAzdataDiscovered;
+ throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
+ return azdataToolService.localAzdata.arc.postgres.server.edit(name, args, additionalEnvVars);
}
}
},
diff --git a/extensions/azdata/src/azdata.ts b/extensions/azdata/src/azdata.ts
index 28d859f753..4ea09ffae7 100644
--- a/extensions/azdata/src/azdata.ts
+++ b/extensions/azdata/src/azdata.ts
@@ -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(['arc', 'postgres', 'server', 'delete', '-n', name]);
+ },
list: async () => {
return this.executeCommand(['arc', 'postgres', 'server', 'list']);
},
show: async (name: string) => {
return this.executeCommand(['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(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 {
+ 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 {
// 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 {
- // 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
diff --git a/extensions/azdata/src/azdataReleaseInfo.ts b/extensions/azdata/src/azdataReleaseInfo.ts
new file mode 100644
index 0000000000..adac99ad7d
--- /dev/null
+++ b/extensions/azdata/src/azdataReleaseInfo.ts
@@ -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 {
+ 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 {
+ 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 {
+ 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)}`);
+ }
+}
diff --git a/extensions/azdata/src/common/childProcess.ts b/extensions/azdata/src/common/childProcess.ts
index b4620c2748..4e6d9a4a21 100644
--- a/extensions/azdata/src/common/childProcess.ts
+++ b/extensions/azdata/src/common/childProcess.ts
@@ -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 {
+ 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(''));
- }
});
});
}
diff --git a/extensions/azdata/src/common/logger.ts b/extensions/azdata/src/common/logger.ts
index c4f716d85e..a80dfca2a7 100644
--- a/extensions/azdata/src/common/logger.ts
+++ b/extensions/azdata/src/common/logger.ts
@@ -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 {
diff --git a/extensions/azdata/src/common/utils.ts b/extensions/azdata/src/common/utils.ts
index 023406586c..20106c28bd 100644
--- a/extensions/azdata/src/common/utils.ts
+++ b/extensions/azdata/src/common/utils.ts
@@ -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
diff --git a/extensions/azdata/src/extension.ts b/extensions/azdata/src/extension.ts
index 0cdb1389b4..8697e3e334 100644
--- a/extensions/azdata/src/extension.ts
+++ b/extensions/azdata/src/extension.ts
@@ -34,15 +34,6 @@ export async function activate(context: vscode.ExtensionContext): Promise {
- 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.
diff --git a/extensions/azdata/src/localizedConstants.ts b/extensions/azdata/src/localizedConstants.ts
index 6f0037cc3c..351ac798e0 100644
--- a/extensions/azdata/src/localizedConstants.ts
+++ b/extensions/azdata/src/localizedConstants.ts
@@ -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);
diff --git a/extensions/azdata/src/test/azdata.test.ts b/extensions/azdata/src/test/azdata.test.ts
index d15414eec7..e1725c82b1 100644
--- a/extensions/azdata/src/test/azdata.test.ts
+++ b/extensions/azdata/src/test/azdata.test.ts
@@ -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(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 {
@@ -77,8 +82,12 @@ describe('azdata', function () {
if (process.platform === 'win32') {
it('unsuccessful download - win32', async function (): Promise {
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(loc.yes));
+ executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand').returns(Promise.resolve({ stdout: '', stderr: '' }));
});
it('successful update', async function (): Promise {
@@ -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 {
- // 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 {
+ 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({
- stderr: '',
- stdout: JSON.stringify(brewInfoOutput)
- });
+ .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();
}
diff --git a/extensions/azdata/src/test/common/httpClient.test.ts b/extensions/azdata/src/test/common/httpClient.test.ts
index 3d79597201..3ddb2c45bd 100644
--- a/extensions/azdata/src/test/common/httpClient.test.ts
+++ b/extensions/azdata/src/test/common/httpClient.test.ts
@@ -73,12 +73,12 @@ describe('HttpClient', function (): void {
});
describe('getTextContent', function (): void {
- it.skip('Gets file contents correctly', async function (): Promise {
+ it('Gets file contents correctly', async function (): Promise {
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 {
diff --git a/extensions/azdata/src/typings/azdata-ext.d.ts b/extensions/azdata/src/typings/azdata-ext.d.ts
index e79ca9e2c7..8eef9ced13 100644
--- a/extensions/azdata/src/typings/azdata-ext.d.ts
+++ b/extensions/azdata/src/typings/azdata-ext.d.ts
@@ -19,7 +19,7 @@ declare module 'azdata-ext' {
export interface ErrorWithLink extends Error {
messageWithLink: string;
}
-
+
export interface DcEndpointListResult {
description: string, // "Management Proxy"
endpoint: string, // "https://10.91.86.39:30777"
@@ -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"
},
- storage: {
- volumeSize: string, // "1Gi"
+ limits: {
+ cpu: string, // "1.5"
+ memory: string // "256Mi"
}
}
- ]
+ }
},
- status: {
- readyPods: string, // "1/1",
- state: string // "Ready"
+ service: {
+ type: string, // "NodePort"
+ port: number // 5432
+ },
+ storage: {
+ 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"
+ 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>,
list(): Promise>,
- show(name: string): Promise>
+ show(name: string): Promise>,
+ 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>
}
},
sql: {