mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
[Port] Sync up arc and azdata extensions with main (#12810)
* Sync up arc and azdata extensions with main * capture 'this' to use retrieveVariable as callback (#12828) * capture 'this' to use retrieveVariable as callback * remove change not needed for #12082 Co-authored-by: Arvind Ranasaria <ranasaria@outlook.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
@@ -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",
|
||||
"<span style=\"color:red\"><font size=\"3\">Please press the \"Run All\" button to run the notebook</font></span>"
|
||||
],
|
||||
@@ -41,7 +41,21 @@
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Check prerequisites**"
|
||||
"### **Prerequisites** \n",
|
||||
"Ensure the following tools are installed and added to PATH before proceeding.\n",
|
||||
" \n",
|
||||
"|Tools|Description|Installation|\n",
|
||||
"|---|---|---|\n",
|
||||
"|Azure Data CLI (azdata) | Command-line tool for installing and managing resources in an Azure Arc cluster |[Installation](https://docs.microsoft.com/sql/azdata/install/deploy-install-azdata) |"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "20fe3985-a01e-461c-bce0-235f7606cc3c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Setup and Check Prerequisites**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "68531b91-ddce-47d7-a1d8-2ddc3d17f3e7"
|
||||
@@ -75,100 +89,20 @@
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"#### **Ensure Postgres Server Group name and password exist**"
|
||||
"### **Set variables**\n",
|
||||
"\n",
|
||||
"#### \n",
|
||||
"\n",
|
||||
"Generated by Azure Data Studio using the values collected in the 'Deploy PostgreSQL Hyperscale - Azure Arc instance' wizard"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "68ec0760-27d1-4ded-9a9f-89077c40b8bb"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"# Required Values\n",
|
||||
"env_var = \"AZDATA_NB_VAR_CONTROLLER_ENDPOINT\" in os.environ\n",
|
||||
"if env_var:\n",
|
||||
" controller_endpoint = os.environ[\"AZDATA_NB_VAR_CONTROLLER_ENDPOINT\"]\n",
|
||||
"else:\n",
|
||||
" sys.exit(f'environment variable: AZDATA_NB_VAR_CONTROLLER_ENDPOINT was not defined. Exiting\\n')\n",
|
||||
"\n",
|
||||
"env_var = \"AZDATA_NB_VAR_CONTROLLER_USERNAME\" in os.environ\n",
|
||||
"if env_var:\n",
|
||||
" controller_username = os.environ[\"AZDATA_NB_VAR_CONTROLLER_USERNAME\"]\n",
|
||||
"else:\n",
|
||||
" sys.exit(f'environment variable: AZDATA_NB_VAR_CONTROLLER_USERNAME was not defined. Exiting\\n')\n",
|
||||
"\n",
|
||||
"env_var = \"AZDATA_NB_VAR_CONTROLLER_PASSWORD\" in os.environ\n",
|
||||
"if env_var:\n",
|
||||
" controller_password = os.environ[\"AZDATA_NB_VAR_CONTROLLER_PASSWORD\"]\n",
|
||||
"else:\n",
|
||||
" sys.exit(f'environment variable: AZDATA_NB_VAR_CONTROLLER_PASSWORD was not defined. Exiting\\n')\n",
|
||||
"\n",
|
||||
"env_var = \"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_NAME\" in os.environ\n",
|
||||
"if env_var:\n",
|
||||
" server_group_name = os.environ[\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_NAME\"]\n",
|
||||
"else:\n",
|
||||
" sys.exit(f'environment variable: AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_NAME was not defined. Exiting\\n')\n",
|
||||
"\n",
|
||||
"env_var = \"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_PASSWORD\" in os.environ\n",
|
||||
"if env_var:\n",
|
||||
" postgres_password = os.environ[\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_PASSWORD\"]\n",
|
||||
"else:\n",
|
||||
" sys.exit(f'environment variable: AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_PASSWORD was not defined. Exiting\\n') \n",
|
||||
"\n",
|
||||
"env_var = \"AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_DATA\" in os.environ\n",
|
||||
"if env_var:\n",
|
||||
" postgres_storage_class_data = os.environ[\"AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_DATA\"]\n",
|
||||
"else:\n",
|
||||
" sys.exit(f'environment variable: AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_DATA was not defined. Exiting\\n') \n",
|
||||
"env_var = \"AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_LOGS\" in os.environ\n",
|
||||
"if env_var:\n",
|
||||
" postgres_storage_class_logs = os.environ[\"AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_LOGS\"]\n",
|
||||
"else:\n",
|
||||
" sys.exit(f'environment variable: AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_LOGS was not defined. Exiting\\n') \n",
|
||||
"env_var = \"AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_BACKUPS\" in os.environ\n",
|
||||
"if env_var:\n",
|
||||
" postgres_storage_class_backups = os.environ[\"AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_BACKUPS\"]\n",
|
||||
"else:\n",
|
||||
" sys.exit(f'environment variable: AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_BACKUPS was not defined. Exiting\\n') \n",
|
||||
""
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "53769960-e1f8-4477-b4cf-3ab1ea34348b",
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"#### **Get optional parameters for the PostgreSQL server group**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "68ec0760-27d1-4ded-9a9f-89077c40b8bb"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"server_group_workers = os.environ[\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_WORKERS\"]\n",
|
||||
"server_group_port = os.environ.get(\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_PORT\")\n",
|
||||
"server_group_cores_request = os.environ.get(\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_REQUEST\")\n",
|
||||
"server_group_cores_limit = os.environ.get(\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_LIMIT\")\n",
|
||||
"server_group_memory_request = os.environ.get(\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_REQUEST\")\n",
|
||||
"server_group_memory_limit = os.environ.get(\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_LIMIT\")"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "53769960-e1f8-4477-b4cf-3ab1ea34348b",
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Installing PostgreSQL server group**"
|
||||
"### **Creating the PostgreSQL Hyperscale - Azure Arc instance**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "90b0e162-2987-463f-9ce6-12dda1267189"
|
||||
@@ -179,7 +113,7 @@
|
||||
"source": [
|
||||
"# Login to the data controller.\n",
|
||||
"#\n",
|
||||
"os.environ[\"AZDATA_PASSWORD\"] = controller_password\n",
|
||||
"os.environ[\"AZDATA_PASSWORD\"] = os.environ[\"AZDATA_NB_VAR_CONTROLLER_PASSWORD\"]\n",
|
||||
"cmd = f'azdata login -e {controller_endpoint} -u {controller_username}'\n",
|
||||
"out=run_command()"
|
||||
],
|
||||
@@ -192,17 +126,22 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"print (f'Creating a PostgreSQL server group on Azure Arc')\n",
|
||||
"print (f'Creating the PostgreSQL Hyperscale - Azure Arc instance')\n",
|
||||
"\n",
|
||||
"workers_option = f' -w {server_group_workers}' if server_group_workers else \"\"\n",
|
||||
"port_option = f' --port \"{server_group_port}\"' if server_group_port else \"\"\n",
|
||||
"cores_request_option = f' -cr \"{server_group_cores_request}\"' if server_group_cores_request else \"\"\n",
|
||||
"cores_limit_option = f' -cl \"{server_group_cores_limit}\"' if server_group_cores_limit else \"\"\n",
|
||||
"memory_request_option = f' -mr \"{server_group_memory_request}Mi\"' if server_group_memory_request else \"\"\n",
|
||||
"memory_limit_option = f' -ml \"{server_group_memory_limit}Mi\"' if server_group_memory_limit else \"\"\n",
|
||||
"workers_option = f' -w {postgres_server_group_workers}' if postgres_server_group_workers else \"\"\n",
|
||||
"port_option = f' --port \"{postgres_server_group_port}\"' if postgres_server_group_port else \"\"\n",
|
||||
"engine_version_option = f' -ev {postgres_server_group_engine_version}' if postgres_server_group_engine_version else \"\"\n",
|
||||
"extensions_option = f' --extensions \"{postgres_server_group_extensions}\"' if postgres_server_group_extensions else \"\"\n",
|
||||
"volume_size_data_option = f' -vsd {postgres_server_group_volume_size_data}Gi' if postgres_server_group_volume_size_data else \"\"\n",
|
||||
"volume_size_logs_option = f' -vsl {postgres_server_group_volume_size_logs}Gi' if postgres_server_group_volume_size_logs else \"\"\n",
|
||||
"volume_size_backups_option = f' -vsb {postgres_server_group_volume_size_backups}Gi' if postgres_server_group_volume_size_backups else \"\"\n",
|
||||
"cores_request_option = f' -cr \"{postgres_server_group_cores_request}\"' if postgres_server_group_cores_request else \"\"\n",
|
||||
"cores_limit_option = f' -cl \"{postgres_server_group_cores_limit}\"' if postgres_server_group_cores_limit else \"\"\n",
|
||||
"memory_request_option = f' -mr \"{postgres_server_group_memory_request}Gi\"' if postgres_server_group_memory_request else \"\"\n",
|
||||
"memory_limit_option = f' -ml \"{postgres_server_group_memory_limit}Gi\"' if postgres_server_group_memory_limit else \"\"\n",
|
||||
"\n",
|
||||
"os.environ[\"AZDATA_PASSWORD\"] = postgres_password\n",
|
||||
"cmd = f'azdata arc postgres server create -n {server_group_name} -scd {postgres_storage_class_data} -scl {postgres_storage_class_logs} -scb {postgres_storage_class_backups}{workers_option}{port_option}{cores_request_option}{cores_limit_option}{memory_request_option}{memory_limit_option}'\n",
|
||||
"os.environ[\"AZDATA_PASSWORD\"] = os.environ[\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_PASSWORD\"]\n",
|
||||
"cmd = f'azdata arc postgres server create -n {postgres_server_group_name} -scd {postgres_storage_class_data} -scl {postgres_storage_class_logs} -scb {postgres_storage_class_backups}{workers_option}{port_option}{engine_version_option}{extensions_option}{volume_size_data_option}{volume_size_logs_option}{volume_size_backups_option}{cores_request_option}{cores_limit_option}{memory_request_option}{memory_limit_option}'\n",
|
||||
"out=run_command()"
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
@@ -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 <a href=\"https://docs.microsoft.com/azure/sql-database/sql-database-managed-instance\">Azure SQL managed instance</a> on an existing Azure Arc data cluster.\n",
|
||||
"This notebook walks through the process of creating a <a href=\"https://docs.microsoft.com/azure/sql-database/sql-database-managed-instance\">SQL managed instance - Azure Arc</a> on an existing Azure Arc Data Controller.\n",
|
||||
" \n",
|
||||
"* Follow the instructions in the **Prerequisites** cell to install the tools if not already installed.\n",
|
||||
"* Make sure you have the target Azure Arc data cluster already created.\n",
|
||||
"* Make sure you have the target Azure Arc Data Controller already created.\n",
|
||||
"\n",
|
||||
"<span style=\"color:red\"><font size=\"3\">Please press the \"Run All\" button to run the notebook</font></span>"
|
||||
],
|
||||
@@ -41,7 +41,21 @@
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Check prerequisites**"
|
||||
"### **Prerequisites** \n",
|
||||
"Ensure the following tools are installed and added to PATH before proceeding.\n",
|
||||
" \n",
|
||||
"|Tools|Description|Installation|\n",
|
||||
"|---|---|---|\n",
|
||||
"|Azure Data CLI (azdata) | Command-line tool for installing and managing resources in an Azure Arc cluster |[Installation](https://docs.microsoft.com/sql/azdata/install/deploy-install-azdata) |"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "d1c8258e-9efd-4380-a48c-cd675423ed2f"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Setup and Check Prerequisites**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "68531b91-ddce-47d7-a1d8-2ddc3d17f3e7"
|
||||
@@ -75,70 +89,20 @@
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"#### **Ensure SQL instance name, username and password exist**"
|
||||
"### **Set variables**\n",
|
||||
"\n",
|
||||
"#### \n",
|
||||
"\n",
|
||||
"Generated by Azure Data Studio using the values collected in the 'Deploy Azure SQL managed instance - Azure Arc' wizard"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "68ec0760-27d1-4ded-9a9f-89077c40b8bb"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"# Required Values\n",
|
||||
"env_var = \"AZDATA_NB_VAR_CONTROLLER_ENDPOINT\" in os.environ\n",
|
||||
"if env_var:\n",
|
||||
" controller_endpoint = os.environ[\"AZDATA_NB_VAR_CONTROLLER_ENDPOINT\"]\n",
|
||||
"else:\n",
|
||||
" sys.exit(f'environment variable: AZDATA_NB_VAR_CONTROLLER_ENDPOINT was not defined. Exiting\\n')\n",
|
||||
"\n",
|
||||
"env_var = \"AZDATA_NB_VAR_CONTROLLER_USERNAME\" in os.environ\n",
|
||||
"if env_var:\n",
|
||||
" controller_username = os.environ[\"AZDATA_NB_VAR_CONTROLLER_USERNAME\"]\n",
|
||||
"else:\n",
|
||||
" sys.exit(f'environment variable: AZDATA_NB_VAR_CONTROLLER_USERNAME was not defined. Exiting\\n')\n",
|
||||
"\n",
|
||||
"env_var = \"AZDATA_NB_VAR_CONTROLLER_PASSWORD\" in os.environ\n",
|
||||
"if env_var:\n",
|
||||
" controller_password = os.environ[\"AZDATA_NB_VAR_CONTROLLER_PASSWORD\"]\n",
|
||||
"else:\n",
|
||||
" sys.exit(f'environment variable: AZDATA_NB_VAR_CONTROLLER_PASSWORD was not defined. Exiting\\n')\n",
|
||||
"\n",
|
||||
"env_var = \"AZDATA_NB_VAR_SQL_INSTANCE_NAME\" in os.environ\n",
|
||||
"if env_var:\n",
|
||||
" mssql_instance_name = os.environ[\"AZDATA_NB_VAR_SQL_INSTANCE_NAME\"]\n",
|
||||
"else:\n",
|
||||
" sys.exit(f'environment variable: AZDATA_NB_VAR_SQL_INSTANCE_NAME was not defined. Exiting\\n')\n",
|
||||
"\n",
|
||||
"env_var = \"AZDATA_NB_VAR_SQL_PASSWORD\" in os.environ\n",
|
||||
"if env_var:\n",
|
||||
" mssql_password = os.environ[\"AZDATA_NB_VAR_SQL_PASSWORD\"]\n",
|
||||
"else:\n",
|
||||
" sys.exit(f'environment variable: AZDATA_NB_VAR_SQL_PASSWORD was not defined. Exiting\\n')\n",
|
||||
"\n",
|
||||
"env_var = \"AZDATA_NB_VAR_SQL_STORAGE_CLASS_DATA\" in os.environ\n",
|
||||
"if env_var:\n",
|
||||
" mssql_storage_class_data = os.environ[\"AZDATA_NB_VAR_SQL_STORAGE_CLASS_DATA\"]\n",
|
||||
"else:\n",
|
||||
" sys.exit(f'environment variable: AZDATA_NB_VAR_SQL_STORAGE_CLASS_DATA was not defined. Exiting\\n')\n",
|
||||
"\n",
|
||||
"env_var = \"AZDATA_NB_VAR_SQL_STORAGE_CLASS_LOGS\" in os.environ\n",
|
||||
"if env_var:\n",
|
||||
" mssql_storage_class_logs = os.environ[\"AZDATA_NB_VAR_SQL_STORAGE_CLASS_LOGS\"]\n",
|
||||
"else:\n",
|
||||
" sys.exit(f'environment variable: AZDATA_NB_VAR_SQL_STORAGE_CLASS_LOGS was not defined. Exiting\\n') \n",
|
||||
""
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "53769960-e1f8-4477-b4cf-3ab1ea34348b",
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Installing Managed SQL Instance**"
|
||||
"### **Creating the SQL managed instance - Azure Arc instance**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "90b0e162-2987-463f-9ce6-12dda1267189"
|
||||
@@ -149,7 +113,7 @@
|
||||
"source": [
|
||||
"# Login to the data controller.\n",
|
||||
"#\n",
|
||||
"os.environ[\"AZDATA_PASSWORD\"] = controller_password\n",
|
||||
"os.environ[\"AZDATA_PASSWORD\"] = os.environ[\"AZDATA_NB_VAR_CONTROLLER_PASSWORD\"]\n",
|
||||
"cmd = f'azdata login -e {controller_endpoint} -u {controller_username}'\n",
|
||||
"out=run_command()"
|
||||
],
|
||||
@@ -162,10 +126,16 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"print (f'Creating Managed SQL Server instance on Azure Arc')\n",
|
||||
"print (f'Creating the SQL managed instance - Azure Arc instance')\n",
|
||||
"\n",
|
||||
"os.environ[\"AZDATA_PASSWORD\"] = mssql_password\n",
|
||||
"cmd = f'azdata arc sql mi create -n {mssql_instance_name} -scd {mssql_storage_class_data} -scl {mssql_storage_class_logs}'\n",
|
||||
"cores_request_option = f' -cr \"{sql_cores_request}\"' if sql_cores_request else \"\"\n",
|
||||
"cores_limit_option = f' -cl \"{sql_cores_limit}\"' if sql_cores_limit else \"\"\n",
|
||||
"memory_request_option = f' -mr \"{sql_memory_request}Gi\"' if sql_memory_request else \"\"\n",
|
||||
"memory_limit_option = f' -ml \"{sql_memory_limit}Gi\"' if sql_memory_limit else \"\"\n",
|
||||
"\n",
|
||||
"os.environ[\"AZDATA_USERNAME\"] = sql_username\n",
|
||||
"os.environ[\"AZDATA_PASSWORD\"] = os.environ[\"AZDATA_NB_VAR_SQL_PASSWORD\"]\n",
|
||||
"cmd = f'azdata arc sql mi create -n {sql_instance_name} -scd {sql_storage_class_data} -scl {sql_storage_class_logs}{cores_request_option}{cores_limit_option}{memory_request_option}{memory_limit_option}'\n",
|
||||
"out=run_command()"
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,15 +148,15 @@ async function promptInputBox(title: string, options: vscode.InputBoxOptions): P
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens an input box prompting the user to enter in the name of a resource to delete
|
||||
* @param name The name of the resource to delete
|
||||
* Opens an input box prompting the user to enter in the name of an instance to delete
|
||||
* @param name The name of the instance to delete
|
||||
* @returns Promise resolving to true if the user confirmed the name, false if the input box was closed for any other reason
|
||||
*/
|
||||
export async function promptForResourceDeletion(name: string): Promise<boolean> {
|
||||
const title = loc.resourceDeletionWarning(name);
|
||||
export async function promptForInstanceDeletion(name: string): Promise<boolean> {
|
||||
const title = loc.instanceDeletionWarning(name);
|
||||
const options: vscode.InputBoxOptions = {
|
||||
placeHolder: name,
|
||||
validateInput: input => input !== name ? loc.invalidResourceDeletionName(name) : ''
|
||||
validateInput: input => input !== name ? loc.invalidInstanceDeletionName(name) : ''
|
||||
};
|
||||
|
||||
return await promptInputBox(title, options) !== undefined;
|
||||
@@ -189,28 +189,15 @@ export async function promptAndConfirmPassword(validate: (input: string) => stri
|
||||
/**
|
||||
* Gets the message to display for a given error object that may be a variety of types.
|
||||
* @param error The error object
|
||||
* @param useMessageWithLink Whether to use the messageWithLink - if available
|
||||
*/
|
||||
export function getErrorMessage(error: any): string {
|
||||
export function getErrorMessage(error: any, useMessageWithLink: boolean = false): string {
|
||||
if (useMessageWithLink && error.messageWithLink) {
|
||||
return error.messageWithLink;
|
||||
}
|
||||
return error.message ?? error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an instance name from the controller. An instance name will either be just its name
|
||||
* e.g. myinstance or namespace_name e.g. mynamespace_my-instance.
|
||||
* @param instanceName The instance name in one of the formats described
|
||||
*/
|
||||
export function parseInstanceName(instanceName: string | undefined): string {
|
||||
instanceName = instanceName ?? '';
|
||||
const parts: string[] = instanceName.split('_');
|
||||
if (parts.length === 2) {
|
||||
instanceName = parts[1];
|
||||
}
|
||||
else if (parts.length > 2) {
|
||||
throw new Error(`Cannot parse resource '${instanceName}'. Acceptable formats are 'namespace_name' or 'name'.`);
|
||||
}
|
||||
return instanceName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an address into its separate ip and port values. Address must be in the form <ip>:<port>
|
||||
* @param address The address to parse
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -28,6 +28,14 @@ export async function activate(context: vscode.ExtensionContext): Promise<arc.IE
|
||||
});
|
||||
|
||||
vscode.commands.registerCommand('arc.connectToController', async () => {
|
||||
const nodes = await treeDataProvider.getChildren();
|
||||
if (nodes.length > 0) {
|
||||
const response = await vscode.window.showErrorMessage(loc.onlyOneControllerSupported, loc.yes, loc.no);
|
||||
if (response !== loc.yes) {
|
||||
return;
|
||||
}
|
||||
await treeDataProvider.removeController(nodes[0] as ControllerTreeNode);
|
||||
}
|
||||
const dialog = new ConnectToControllerDialog(treeDataProvider);
|
||||
dialog.showDialog();
|
||||
const model = await dialog.waitForClose();
|
||||
|
||||
@@ -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)); }
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { ControllerInfo, ResourceType } from 'arc';
|
||||
import * as azdataExt from 'azdata-ext';
|
||||
import * as vscode from 'vscode';
|
||||
import { parseInstanceName, UserCancelledError } from '../common/utils';
|
||||
import { UserCancelledError } from '../common/utils';
|
||||
import * as loc from '../localizedConstants';
|
||||
import { ConnectToControllerDialog } from '../ui/dialogs/connectControllerDialog';
|
||||
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
|
||||
@@ -20,7 +20,6 @@ export type Registration = {
|
||||
export class ControllerModel {
|
||||
private readonly _azdataApi: azdataExt.IExtension;
|
||||
private _endpoints: azdataExt.DcEndpointListResult[] = [];
|
||||
private _namespace: string = '';
|
||||
private _registrations: Registration[] = [];
|
||||
private _controllerConfig: azdataExt.DcConfigShowResult | undefined = undefined;
|
||||
|
||||
@@ -93,7 +92,7 @@ export class ControllerModel {
|
||||
}
|
||||
public async refresh(showErrors: boolean = true, promptReconnect: boolean = false): Promise<void> {
|
||||
await this.azdataLogin(promptReconnect);
|
||||
this._registrations = [];
|
||||
const newRegistrations: Registration[] = [];
|
||||
await Promise.all([
|
||||
this._azdataApi.azdata.arc.dc.config.show().then(result => {
|
||||
this._controllerConfig = result.result;
|
||||
@@ -125,7 +124,7 @@ export class ControllerModel {
|
||||
}),
|
||||
Promise.all([
|
||||
this._azdataApi.azdata.arc.postgres.server.list().then(result => {
|
||||
this._registrations.push(...result.result.map(r => {
|
||||
newRegistrations.push(...result.result.map(r => {
|
||||
return {
|
||||
instanceName: r.name,
|
||||
state: r.state,
|
||||
@@ -134,7 +133,7 @@ export class ControllerModel {
|
||||
}));
|
||||
}),
|
||||
this._azdataApi.azdata.arc.sql.mi.list().then(result => {
|
||||
this._registrations.push(...result.result.map(r => {
|
||||
newRegistrations.push(...result.result.map(r => {
|
||||
return {
|
||||
instanceName: r.name,
|
||||
state: r.state,
|
||||
@@ -143,6 +142,7 @@ export class ControllerModel {
|
||||
}));
|
||||
})
|
||||
]).then(() => {
|
||||
this._registrations = newRegistrations;
|
||||
this.registrationsLastUpdated = new Date();
|
||||
this._onRegistrationsUpdated.fire(this._registrations);
|
||||
})
|
||||
@@ -157,10 +157,6 @@ export class ControllerModel {
|
||||
return this._endpoints.find(e => e.name === name);
|
||||
}
|
||||
|
||||
public get namespace(): string {
|
||||
return this._namespace;
|
||||
}
|
||||
|
||||
public get registrations(): Registration[] {
|
||||
return this._registrations;
|
||||
}
|
||||
@@ -171,19 +167,10 @@ export class ControllerModel {
|
||||
|
||||
public getRegistration(type: ResourceType, name: string): Registration | undefined {
|
||||
return this._registrations.find(r => {
|
||||
return r.instanceType === type && parseInstanceName(r.instanceName) === name;
|
||||
return r.instanceType === type && r.instanceName === name;
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteRegistration(_type: ResourceType, _name: string) {
|
||||
/* TODO chgagnon
|
||||
if (r && !r.isDeleted && r.customObjectName) {
|
||||
const r = this.getRegistration(type, name);
|
||||
await this._registrationRouter.apiV1RegistrationNsNameIsDeletedDelete(this._namespace, r.customObjectName, true);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* property to for use a display label for this controller
|
||||
*/
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ResourceInfo } from 'arc';
|
||||
import { MiaaResourceInfo } from 'arc';
|
||||
import * as azdata from 'azdata';
|
||||
import * as azdataExt from 'azdata-ext';
|
||||
import * as vscode from 'vscode';
|
||||
import { Deferred } from '../common/promise';
|
||||
import { UserCancelledError } from '../common/utils';
|
||||
import { createCredentialId, parseIpAndPort, UserCancelledError } from '../common/utils';
|
||||
import { credentialNamespace } from '../constants';
|
||||
import * as loc from '../localizedConstants';
|
||||
import { ConnectToSqlDialog } from '../ui/dialogs/connectSqlDialog';
|
||||
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
|
||||
import { ControllerModel, Registration } from './controllerModel';
|
||||
import { ResourceModel } from './resourceModel';
|
||||
@@ -35,8 +37,8 @@ export class MiaaModel extends ResourceModel {
|
||||
|
||||
private _refreshPromise: Deferred<void> | undefined = undefined;
|
||||
|
||||
constructor(private _controllerModel: ControllerModel, info: ResourceInfo, registration: Registration, private _treeDataProvider: AzureArcTreeDataProvider) {
|
||||
super(info, registration);
|
||||
constructor(private _controllerModel: ControllerModel, private _miaaInfo: MiaaResourceInfo, registration: Registration, private _treeDataProvider: AzureArcTreeDataProvider) {
|
||||
super(_miaaInfo, registration);
|
||||
this._azdataApi = <azdataExt.IExtension>vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
|
||||
}
|
||||
|
||||
@@ -155,53 +157,15 @@ export class MiaaModel extends ResourceModel {
|
||||
if (this._connectionProfile) {
|
||||
return;
|
||||
}
|
||||
let connection: azdata.connection.ConnectionProfile | azdata.connection.Connection | undefined;
|
||||
|
||||
if (this.info.connectionId) {
|
||||
try {
|
||||
const connections = await azdata.connection.getConnections();
|
||||
const existingConnection = connections.find(conn => conn.connectionId === this.info.connectionId);
|
||||
if (existingConnection) {
|
||||
const credentials = await azdata.connection.getCredentials(this.info.connectionId);
|
||||
if (credentials) {
|
||||
existingConnection.options['password'] = credentials.password;
|
||||
connection = existingConnection;
|
||||
} else {
|
||||
// We need the password so prompt the user for it
|
||||
const connectionProfile: azdata.IConnectionProfile = {
|
||||
serverName: existingConnection.options['serverName'],
|
||||
databaseName: existingConnection.options['databaseName'],
|
||||
authenticationType: existingConnection.options['authenticationType'],
|
||||
providerName: 'MSSQL',
|
||||
connectionName: '',
|
||||
userName: existingConnection.options['user'],
|
||||
password: '',
|
||||
savePassword: false,
|
||||
groupFullName: undefined,
|
||||
saveProfile: true,
|
||||
id: '',
|
||||
groupId: undefined,
|
||||
options: existingConnection.options
|
||||
};
|
||||
connection = await azdata.connection.openConnectionDialog(['MSSQL'], connectionProfile);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore - the connection may not necessarily exist anymore and in that case we'll just reprompt for a connection
|
||||
}
|
||||
}
|
||||
|
||||
if (!connection) {
|
||||
// We need the password so prompt the user for it
|
||||
const connectionProfile: azdata.IConnectionProfile = {
|
||||
// TODO chgagnon fill in external IP and port
|
||||
// serverName: (this.registration.externalIp && this.registration.externalPort) ? `${this.registration.externalIp},${this.registration.externalPort}` : '',
|
||||
serverName: '',
|
||||
const ipAndPort = parseIpAndPort(this.config?.status.externalEndpoint || '');
|
||||
let connectionProfile: azdata.IConnectionProfile | undefined = {
|
||||
serverName: `${ipAndPort.ip},${ipAndPort.port}`,
|
||||
databaseName: '',
|
||||
authenticationType: 'SqlLogin',
|
||||
providerName: 'MSSQL',
|
||||
connectionName: '',
|
||||
userName: 'sa',
|
||||
userName: this._miaaInfo.userName || '',
|
||||
password: '',
|
||||
savePassword: true,
|
||||
groupFullName: undefined,
|
||||
@@ -210,28 +174,41 @@ export class MiaaModel extends ResourceModel {
|
||||
groupId: undefined,
|
||||
options: {}
|
||||
};
|
||||
// Weren't able to load the existing connection so prompt user for new one
|
||||
connection = await azdata.connection.openConnectionDialog(['MSSQL'], connectionProfile);
|
||||
|
||||
// If we have the ID stored then try to retrieve the password from previous connections
|
||||
if (this.info.connectionId) {
|
||||
try {
|
||||
const credentialProvider = await azdata.credentials.getProvider(credentialNamespace);
|
||||
const credentials = await credentialProvider.readCredential(createCredentialId(this._controllerModel.info.id, this.info.resourceType, this.info.name));
|
||||
if (credentials.password) {
|
||||
// Try to connect to verify credentials are still valid
|
||||
connectionProfile.password = credentials.password;
|
||||
// If we don't have a username for some reason then just continue on and we'll prompt for the username below
|
||||
if (connectionProfile.userName) {
|
||||
const result = await azdata.connection.connect(connectionProfile, false, false);
|
||||
if (!result.connected) {
|
||||
vscode.window.showErrorMessage(loc.connectToSqlFailed(connectionProfile.serverName, result.errorMessage));
|
||||
const connectToSqlDialog = new ConnectToSqlDialog(this._controllerModel, this);
|
||||
connectToSqlDialog.showDialog(connectionProfile);
|
||||
connectionProfile = await connectToSqlDialog.waitForClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Unexpected error fetching password for MIAA instance ${err}`);
|
||||
// ignore - something happened fetching the password so just reprompt
|
||||
}
|
||||
}
|
||||
|
||||
if (connection) {
|
||||
const profile = {
|
||||
// The option name might be different here based on where it came from
|
||||
serverName: connection.options['serverName'] || connection.options['server'],
|
||||
databaseName: connection.options['databaseName'] || connection.options['database'],
|
||||
authenticationType: connection.options['authenticationType'],
|
||||
providerName: 'MSSQL',
|
||||
connectionName: '',
|
||||
userName: connection.options['user'],
|
||||
password: connection.options['password'],
|
||||
savePassword: false,
|
||||
groupFullName: undefined,
|
||||
saveProfile: true,
|
||||
id: connection.connectionId,
|
||||
groupId: undefined,
|
||||
options: connection.options
|
||||
};
|
||||
this.updateConnectionProfile(profile);
|
||||
if (!connectionProfile?.userName || !connectionProfile?.password) {
|
||||
// Need to prompt user for password since we don't have one stored
|
||||
const connectToSqlDialog = new ConnectToSqlDialog(this._controllerModel, this);
|
||||
connectToSqlDialog.showDialog(connectionProfile);
|
||||
connectionProfile = await connectToSqlDialog.waitForClose();
|
||||
}
|
||||
|
||||
if (connectionProfile) {
|
||||
this.updateConnectionProfile(connectionProfile);
|
||||
} else {
|
||||
throw new UserCancelledError();
|
||||
}
|
||||
@@ -240,6 +217,7 @@ export class MiaaModel extends ResourceModel {
|
||||
private async updateConnectionProfile(connectionProfile: azdata.IConnectionProfile): Promise<void> {
|
||||
this._connectionProfile = connectionProfile;
|
||||
this.info.connectionId = connectionProfile.id;
|
||||
this._miaaInfo.userName = connectionProfile.userName;
|
||||
await this._treeDataProvider.saveControllers();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,278 +4,83 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ResourceInfo } from 'arc';
|
||||
import * as azdataExt from 'azdata-ext';
|
||||
import * as vscode from 'vscode';
|
||||
import * as loc from '../localizedConstants';
|
||||
import { Registration } from './controllerModel';
|
||||
import { ControllerModel, Registration } from './controllerModel';
|
||||
import { ResourceModel } from './resourceModel';
|
||||
|
||||
export enum PodRole {
|
||||
Monitor,
|
||||
Router,
|
||||
Shard
|
||||
}
|
||||
|
||||
export interface V1Pod {
|
||||
'apiVersion'?: string;
|
||||
'kind'?: string;
|
||||
'metadata'?: any; // V1ObjectMeta;
|
||||
'spec'?: any; // V1PodSpec;
|
||||
'status'?: V1PodStatus;
|
||||
}
|
||||
|
||||
export interface V1PodStatus {
|
||||
'conditions'?: any[]; // Array<V1PodCondition>;
|
||||
'containerStatuses'?: Array<V1ContainerStatus>;
|
||||
'ephemeralContainerStatuses'?: any[]; // Array<V1ContainerStatus>;
|
||||
'hostIP'?: string;
|
||||
'initContainerStatuses'?: any[]; // Array<V1ContainerStatus>;
|
||||
'message'?: string;
|
||||
'nominatedNodeName'?: string;
|
||||
'phase'?: string;
|
||||
'podIP'?: string;
|
||||
'podIPs'?: any[]; // Array<V1PodIP>;
|
||||
'qosClass'?: string;
|
||||
'reason'?: string;
|
||||
'startTime'?: Date | null;
|
||||
}
|
||||
|
||||
export interface V1ContainerStatus {
|
||||
'containerID'?: string;
|
||||
'image'?: string;
|
||||
'imageID'?: string;
|
||||
'lastState'?: any; // V1ContainerState;
|
||||
'name'?: string;
|
||||
'ready'?: boolean;
|
||||
'restartCount'?: number;
|
||||
'started'?: boolean | null;
|
||||
'state'?: any; // V1ContainerState;
|
||||
}
|
||||
|
||||
export interface DuskyObjectModelsDatabaseService {
|
||||
'apiVersion'?: string;
|
||||
'kind'?: string;
|
||||
'metadata'?: any; // V1ObjectMeta;
|
||||
'spec'?: any; // DuskyObjectModelsDatabaseServiceSpec;
|
||||
'status'?: any; // DuskyObjectModelsDatabaseServiceStatus;
|
||||
'arc'?: any; // DuskyObjectModelsDatabaseServiceArcPayload;
|
||||
}
|
||||
|
||||
export interface V1Status {
|
||||
'apiVersion'?: string;
|
||||
'code'?: number | null;
|
||||
'details'?: any; // V1StatusDetails;
|
||||
'kind'?: string;
|
||||
'message'?: string;
|
||||
'metadata'?: any; // V1ListMeta;
|
||||
'reason'?: string;
|
||||
'status'?: string;
|
||||
'hasObject'?: boolean;
|
||||
}
|
||||
|
||||
export interface DuskyObjectModelsDatabase {
|
||||
'name'?: string;
|
||||
'owner'?: string;
|
||||
'sharded'?: boolean | null;
|
||||
}
|
||||
import { parseIpAndPort } from '../common/utils';
|
||||
|
||||
export class PostgresModel extends ResourceModel {
|
||||
private _service?: DuskyObjectModelsDatabaseService;
|
||||
private _pods?: V1Pod[];
|
||||
private readonly _onServiceUpdated = new vscode.EventEmitter<DuskyObjectModelsDatabaseService>();
|
||||
private readonly _onPodsUpdated = new vscode.EventEmitter<V1Pod[]>();
|
||||
public onServiceUpdated = this._onServiceUpdated.event;
|
||||
public onPodsUpdated = this._onPodsUpdated.event;
|
||||
public serviceLastUpdated?: Date;
|
||||
public podsLastUpdated?: Date;
|
||||
private _config?: azdataExt.PostgresServerShowResult;
|
||||
private readonly _azdataApi: azdataExt.IExtension;
|
||||
|
||||
constructor(info: ResourceInfo, registration: Registration) {
|
||||
private readonly _onConfigUpdated = new vscode.EventEmitter<azdataExt.PostgresServerShowResult>();
|
||||
public onConfigUpdated = this._onConfigUpdated.event;
|
||||
public configLastUpdated?: Date;
|
||||
|
||||
constructor(private _controllerModel: ControllerModel, info: ResourceInfo, registration: Registration) {
|
||||
super(info, registration);
|
||||
this._azdataApi = <azdataExt.IExtension>vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
|
||||
}
|
||||
|
||||
/** Returns the service's Kubernetes namespace */
|
||||
public get namespace(): string | undefined {
|
||||
return ''; // TODO chgagnon return this.info.namespace;
|
||||
/** Returns the configuration of Postgres */
|
||||
public get config(): azdataExt.PostgresServerShowResult | undefined {
|
||||
return this._config;
|
||||
}
|
||||
|
||||
/** Returns the service's name */
|
||||
public get name(): string {
|
||||
return this.info.name;
|
||||
/** Returns the major version of Postgres */
|
||||
public get engineVersion(): string | undefined {
|
||||
const kind = this._config?.kind;
|
||||
return kind
|
||||
? kind.substring(kind.lastIndexOf('-') + 1)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/** Returns the service's fully qualified name in the format namespace.name */
|
||||
public get fullName(): string {
|
||||
return `${this.namespace}.${this.name}`;
|
||||
/** Returns the IP address and port of Postgres */
|
||||
public get endpoint(): { ip: string, port: string } | undefined {
|
||||
return this._config?.status.externalEndpoint
|
||||
? parseIpAndPort(this._config.status.externalEndpoint)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/** Returns the service's spec */
|
||||
public get service(): DuskyObjectModelsDatabaseService | undefined {
|
||||
return this._service;
|
||||
/** Returns the scale configuration of Postgres e.g. '3 nodes, 1.5 vCores, 1Gi RAM, 2Gi storage per node' */
|
||||
public get scaleConfiguration(): string | undefined {
|
||||
if (!this._config) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Returns the service's pods */
|
||||
public get pods(): V1Pod[] | undefined {
|
||||
return this._pods;
|
||||
}
|
||||
|
||||
/** Refreshes the model */
|
||||
public async refresh() {
|
||||
await Promise.all([
|
||||
/* TODO enable
|
||||
this._databaseRouter.getDuskyDatabaseService(this.info.namespace || 'test', this.info.name).then(response => {
|
||||
this._service = response.body;
|
||||
this.serviceLastUpdated = new Date();
|
||||
this._onServiceUpdated.fire(this._service);
|
||||
}),
|
||||
this._databaseRouter.getDuskyPods(this.info.namespace || 'test', this.info.name).then(response => {
|
||||
this._pods = response.body;
|
||||
this.podsLastUpdated = new Date();
|
||||
this._onPodsUpdated.fire(this._pods!);
|
||||
})
|
||||
*/
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the service
|
||||
* @param func A function of modifications to apply to the service
|
||||
*/
|
||||
public async update(_func: (service: DuskyObjectModelsDatabaseService) => void): Promise<DuskyObjectModelsDatabaseService> {
|
||||
return <any>undefined;
|
||||
/*
|
||||
// Get the latest spec of the service in case it has changed
|
||||
const service = (await this._databaseRouter.getDuskyDatabaseService(this.info.namespace || 'test', this.info.name)).body;
|
||||
service.status = undefined; // can't update the status
|
||||
func(service);
|
||||
|
||||
return await this._databaseRouter.updateDuskyDatabaseService(this.namespace || 'test', this.name, service).then(r => {
|
||||
this._service = r.body;
|
||||
return this._service;
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
/** Deletes the service */
|
||||
public async delete(): Promise<V1Status> {
|
||||
return <any>undefined;
|
||||
// return (await this._databaseRouter.deleteDuskyDatabaseService(this.info.namespace || 'test', this.info.name)).body;
|
||||
}
|
||||
|
||||
/** Creates a SQL database in the service */
|
||||
public async createDatabase(_db: DuskyObjectModelsDatabase): Promise<DuskyObjectModelsDatabase> {
|
||||
return <any>undefined;
|
||||
// return (await this._databaseRouter.createDuskyDatabase(this.namespace || 'test', this.name, db)).body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the IP address and port of the service, preferring external IP over
|
||||
* internal IP. If either field is not available it will be set to undefined.
|
||||
*/
|
||||
public get endpoint(): { ip?: string, port?: number } {
|
||||
const externalIp = this._service?.status?.externalIP;
|
||||
const internalIp = this._service?.status?.internalIP;
|
||||
const externalPort = this._service?.status?.externalPort;
|
||||
const internalPort = this._service?.status?.internalPort;
|
||||
|
||||
return externalIp ? { ip: externalIp, port: externalPort ?? undefined }
|
||||
: internalIp ? { ip: internalIp, port: internalPort ?? undefined }
|
||||
: { ip: undefined, port: undefined };
|
||||
}
|
||||
|
||||
/** Returns the service's configuration e.g. '3 nodes, 1.5 vCores, 1GiB RAM, 2GiB storage per node' */
|
||||
public get configuration(): string {
|
||||
|
||||
// TODO: Resource requests and limits can be configured per role. Figure out how
|
||||
// to display that in the UI. For now, only show the default configuration.
|
||||
const cpuLimit = this._service?.spec?.scheduling?._default?.resources?.limits?.['cpu'];
|
||||
const ramLimit = this._service?.spec?.scheduling?._default?.resources?.limits?.['memory'];
|
||||
const cpuRequest = this._service?.spec?.scheduling?._default?.resources?.requests?.['cpu'];
|
||||
const ramRequest = this._service?.spec?.scheduling?._default?.resources?.requests?.['memory'];
|
||||
const storage = this._service?.spec?.storage?.volumeSize;
|
||||
const nodes = this.pods?.length;
|
||||
const cpuLimit = this._config.spec.scheduling?.default?.resources?.limits?.cpu;
|
||||
const ramLimit = this._config.spec.scheduling?.default?.resources?.limits?.memory;
|
||||
const cpuRequest = this._config.spec.scheduling?.default?.resources?.requests?.cpu;
|
||||
const ramRequest = this._config.spec.scheduling?.default?.resources?.requests?.memory;
|
||||
const storage = this._config.spec.storage?.data?.size;
|
||||
const nodes = (this._config.spec.scale?.shards ?? 0) + 1; // An extra node for the coordinator
|
||||
|
||||
let configuration: string[] = [];
|
||||
|
||||
if (nodes) {
|
||||
configuration.push(`${nodes} ${nodes > 1 ? loc.nodes : loc.node}`);
|
||||
}
|
||||
|
||||
// Prefer limits if they're provided, otherwise use requests if they're provided
|
||||
if (cpuLimit || cpuRequest) {
|
||||
configuration.push(`${this.formatCores(cpuLimit ?? cpuRequest!)} ${loc.vCores}`);
|
||||
configuration.push(`${cpuLimit ?? cpuRequest!} ${loc.vCores}`);
|
||||
}
|
||||
|
||||
if (ramLimit || ramRequest) {
|
||||
configuration.push(`${this.formatMemory(ramLimit ?? ramRequest!)} ${loc.ram}`);
|
||||
configuration.push(`${ramLimit ?? ramRequest!} ${loc.ram}`);
|
||||
}
|
||||
|
||||
if (storage) {
|
||||
configuration.push(`${this.formatMemory(storage)} ${loc.storagePerNode}`);
|
||||
configuration.push(`${storage} ${loc.storagePerNode}`);
|
||||
}
|
||||
|
||||
return configuration.join(', ');
|
||||
}
|
||||
|
||||
/** Given a V1Pod, returns its PodRole or undefined if the role isn't known */
|
||||
public static getPodRole(pod: V1Pod): PodRole | undefined {
|
||||
const name = pod.metadata?.name;
|
||||
const role = name?.substring(name.lastIndexOf('-'))[1];
|
||||
switch (role) {
|
||||
case 'm': return PodRole.Monitor;
|
||||
case 'r': return PodRole.Router;
|
||||
case 's': return PodRole.Shard;
|
||||
default: return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** Given a PodRole, returns its localized name */
|
||||
public static getPodRoleName(role?: PodRole): string {
|
||||
switch (role) {
|
||||
case PodRole.Monitor: return loc.monitor;
|
||||
case PodRole.Router: return loc.coordinator;
|
||||
case PodRole.Shard: return loc.worker;
|
||||
default: return '';
|
||||
}
|
||||
}
|
||||
|
||||
/** Given a V1Pod returns its status */
|
||||
public static getPodStatus(pod: V1Pod): string {
|
||||
const phase = pod.status?.phase;
|
||||
if (phase !== 'Running') {
|
||||
return phase ?? '';
|
||||
}
|
||||
|
||||
// Pods can be in the running phase while some
|
||||
// containers are crashing, so check those too.
|
||||
for (let c of pod.status?.containerStatuses?.filter(c => !c.ready) ?? []) {
|
||||
const wReason = c.state?.waiting?.reason;
|
||||
const tReason = c.state?.terminated?.reason;
|
||||
if (wReason) { return wReason; }
|
||||
if (tReason) { return tReason; }
|
||||
}
|
||||
|
||||
return loc.running;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts millicores to cores (600m -> 0.6 cores)
|
||||
* https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-cpu
|
||||
* @param cores The millicores to format e.g. 600m
|
||||
*/
|
||||
private formatCores(cores: string): number {
|
||||
return cores?.endsWith('m') ? +cores.slice(0, -1) / 1000 : +cores;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the memory to end with 'B' e.g:
|
||||
* 1 -> 1B
|
||||
* 1K -> 1KB, 1Ki -> 1KiB
|
||||
* 1M -> 1MB, 1Mi -> 1MiB
|
||||
* 1G -> 1GB, 1Gi -> 1GiB
|
||||
* 1T -> 1TB, 1Ti -> 1TiB
|
||||
* https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-memory
|
||||
* @param memory The amount + unit of memory to format e.g. 1K
|
||||
*/
|
||||
private formatMemory(memory: string): string {
|
||||
return memory && !memory.endsWith('B') ? `${memory}B` : memory;
|
||||
/** Refreshes the model */
|
||||
public async refresh() {
|
||||
await this._controllerModel.azdataLogin();
|
||||
this._config = (await this._azdataApi.azdata.arc.postgres.server.show(this.info.name)).result;
|
||||
this.configLastUpdated = new Date();
|
||||
this._onConfigUpdated.fire(this._config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,9 @@ export class ArcControllersOptionsSourceProvider implements rd.IOptionsSourcePro
|
||||
}
|
||||
|
||||
getVariableValue(variableName: string, controllerLabel: string): Promise<string> {
|
||||
return this._cacheManager.getCacheEntry(JSON.stringify([variableName, controllerLabel]), this.retrieveVariable);
|
||||
// capture 'this' in an arrow function object
|
||||
const retrieveVariable = (key: string) => this.retrieveVariable(key);
|
||||
return this._cacheManager.getCacheEntry(JSON.stringify([variableName, controllerLabel]), retrieveVariable);
|
||||
}
|
||||
|
||||
private async getPassword(controller: arc.DataController): Promise<string> {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ResourceType } from 'arc';
|
||||
import 'mocha';
|
||||
import * as should from 'should';
|
||||
import * as vscode from 'vscode';
|
||||
import { getAzurecoreApi, getConnectionModeDisplayText, getDatabaseStateDisplayText, getErrorMessage, getResourceTypeIcon, parseEndpoint, parseInstanceName, parseIpAndPort, promptAndConfirmPassword, promptForResourceDeletion, resourceTypeToDisplayName } from '../../common/utils';
|
||||
import { getAzurecoreApi, getConnectionModeDisplayText, getDatabaseStateDisplayText, getErrorMessage, getResourceTypeIcon, parseEndpoint, parseIpAndPort, promptAndConfirmPassword, promptForInstanceDeletion, resourceTypeToDisplayName } from '../../common/utils';
|
||||
import { ConnectionMode as ConnectionMode, IconPathHelper } from '../../constants';
|
||||
import * as loc from '../../localizedConstants';
|
||||
import { MockInputBox } from '../stubs';
|
||||
@@ -47,24 +47,6 @@ describe('parseEndpoint Method Tests', function (): void {
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseInstanceName Method Tests', () => {
|
||||
it('Should parse valid instanceName with namespace correctly', function (): void {
|
||||
should(parseInstanceName('mynamespace_myinstance')).equal('myinstance');
|
||||
});
|
||||
|
||||
it('Should parse valid instanceName without namespace correctly', function (): void {
|
||||
should(parseInstanceName('myinstance')).equal('myinstance');
|
||||
});
|
||||
|
||||
it('Should return empty string when undefined value passed in', function (): void {
|
||||
should(parseInstanceName(undefined)).equal('');
|
||||
});
|
||||
|
||||
it('Should return empty string when empty string value passed in', function (): void {
|
||||
should(parseInstanceName('')).equal('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAzurecoreApi Method Tests', function () {
|
||||
it('Should get azurecore API correctly', function (): void {
|
||||
should(getAzurecoreApi()).not.be.undefined();
|
||||
@@ -140,7 +122,7 @@ describe('promptForResourceDeletion Method Tests', function (): void {
|
||||
});
|
||||
|
||||
it('Resolves as true when value entered is correct', function (done): void {
|
||||
promptForResourceDeletion('myname').then((value: boolean) => {
|
||||
promptForInstanceDeletion('myname').then((value: boolean) => {
|
||||
value ? done() : done(new Error('Expected return value to be true'));
|
||||
});
|
||||
mockInputBox.value = 'myname';
|
||||
@@ -148,14 +130,14 @@ describe('promptForResourceDeletion Method Tests', function (): void {
|
||||
});
|
||||
|
||||
it('Resolves as false when input box is closed early', function (done): void {
|
||||
promptForResourceDeletion('myname').then((value: boolean) => {
|
||||
promptForInstanceDeletion('myname').then((value: boolean) => {
|
||||
!value ? done() : done(new Error('Expected return value to be false'));
|
||||
});
|
||||
mockInputBox.hide();
|
||||
});
|
||||
|
||||
it('Validation message is set when value entered is incorrect', async function (): Promise<void> {
|
||||
promptForResourceDeletion('myname');
|
||||
promptForInstanceDeletion('myname');
|
||||
mockInputBox.value = 'wrong value';
|
||||
await mockInputBox.triggerAccept();
|
||||
should(mockInputBox.validationMessage).not.be.equal('', 'Validation message should not be empty after incorrect value entered');
|
||||
@@ -260,22 +242,6 @@ describe('getErrorMessage Method Tests', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseInstanceName Method Tests', function () {
|
||||
it('2 part name', function (): void {
|
||||
const name = 'MyName';
|
||||
should(parseInstanceName(`MyNamespace_${name}`)).equal(name);
|
||||
});
|
||||
|
||||
it('1 part name', function (): void {
|
||||
const name = 'MyName';
|
||||
should(parseInstanceName(name)).equal(name);
|
||||
});
|
||||
|
||||
it('Invalid name', function (): void {
|
||||
should(() => parseInstanceName('Some_Invalid_Name')).throwError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseIpAndPort', function (): void {
|
||||
it('Valid address', function (): void {
|
||||
const ip = '127.0.0.1';
|
||||
|
||||
77
extensions/arc/src/test/mocks/fakeAzdataApi.ts
Normal file
77
extensions/arc/src/test/mocks/fakeAzdataApi.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdataExt from 'azdata-ext';
|
||||
|
||||
/**
|
||||
* Simple fake Azdata Api used to mock the API during tests
|
||||
*/
|
||||
export class FakeAzdataApi implements azdataExt.IAzdataApi {
|
||||
|
||||
public postgresInstances: azdataExt.PostgresServerListResult[] = [];
|
||||
public miaaInstances: azdataExt.SqlMiListResult[] = [];
|
||||
|
||||
//
|
||||
// API Implementation
|
||||
//
|
||||
public get arc() {
|
||||
const self = this;
|
||||
return {
|
||||
dc: {
|
||||
create(_namespace: string, _name: string, _connectivityMode: string, _resourceGroup: string, _location: string, _subscription: string, _profileName?: string, _storageClass?: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
|
||||
endpoint: {
|
||||
async list(): Promise<azdataExt.AzdataOutput<azdataExt.DcEndpointListResult[]>> { return <any>{ result: [] }; }
|
||||
},
|
||||
config: {
|
||||
list(): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigListResult[]>> { throw new Error('Method not implemented.'); },
|
||||
async show(): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigShowResult>> { return <any>{ result: undefined! }; }
|
||||
}
|
||||
},
|
||||
postgres: {
|
||||
server: {
|
||||
delete(_name: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
|
||||
async list(): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerListResult[]>> { return <any>{ result: self.postgresInstances }; },
|
||||
show(_name: string): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerShowResult>> { throw new Error('Method not implemented.'); },
|
||||
edit(
|
||||
_name: string,
|
||||
_args: {
|
||||
adminPassword?: boolean,
|
||||
coresLimit?: string,
|
||||
coresRequest?: string,
|
||||
engineSettings?: string,
|
||||
extensions?: string,
|
||||
memoryLimit?: string,
|
||||
memoryRequest?: string,
|
||||
noWait?: boolean,
|
||||
port?: number,
|
||||
replaceEngineSettings?: boolean,
|
||||
workers?: number
|
||||
},
|
||||
_additionalEnvVars?: { [key: string]: string }): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); }
|
||||
}
|
||||
},
|
||||
sql: {
|
||||
mi: {
|
||||
delete(_name: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
|
||||
async list(): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiListResult[]>> { return <any>{ result: self.miaaInstances }; },
|
||||
show(_name: string): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiShowResult>> { throw new Error('Method not implemented.'); }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
getPath(): Promise<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
login(_endpoint: string, _username: string, _password: string): Promise<azdataExt.AzdataOutput<any>> {
|
||||
return <any>undefined;
|
||||
}
|
||||
version(): Promise<azdataExt.AzdataOutput<string>> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
getSemVersion(): any {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,16 +3,21 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ControllerInfo } from 'arc';
|
||||
import { ControllerInfo, ResourceType } from 'arc';
|
||||
import 'mocha';
|
||||
import * as should from 'should';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
import * as sinon from 'sinon';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import * as vscode from 'vscode';
|
||||
import * as azdataExt from 'azdata-ext';
|
||||
import { ControllerModel } from '../../../models/controllerModel';
|
||||
import { MiaaModel } from '../../../models/miaaModel';
|
||||
import { AzureArcTreeDataProvider } from '../../../ui/tree/azureArcTreeDataProvider';
|
||||
import { ControllerTreeNode } from '../../../ui/tree/controllerTreeNode';
|
||||
import { MiaaTreeNode } from '../../../ui/tree/miaaTreeNode';
|
||||
import { FakeControllerModel } from '../../mocks/fakeControllerModel';
|
||||
import { FakeAzdataApi } from '../../mocks/fakeAzdataApi';
|
||||
|
||||
describe('AzureArcTreeDataProvider tests', function (): void {
|
||||
let treeDataProvider: AzureArcTreeDataProvider;
|
||||
@@ -84,6 +89,27 @@ describe('AzureArcTreeDataProvider tests', function (): void {
|
||||
let children = await treeDataProvider.getChildren();
|
||||
should(children.length).equal(0, 'After loading we should have 0 children');
|
||||
});
|
||||
|
||||
it('should return all children of controller after loading', async function (): Promise<void> {
|
||||
const mockArcExtension = TypeMoq.Mock.ofType<vscode.Extension<any>>();
|
||||
const mockArcApi = TypeMoq.Mock.ofType<azdataExt.IExtension>();
|
||||
mockArcExtension.setup(x => x.exports).returns(() => {
|
||||
return mockArcApi.object;
|
||||
});
|
||||
const fakeAzdataApi = new FakeAzdataApi();
|
||||
fakeAzdataApi.postgresInstances = [{ name: 'pg1', state: '', workers: 0 }];
|
||||
fakeAzdataApi.miaaInstances = [{ name: 'miaa1', state: '', replicas: '', serverEndpoint: '' }];
|
||||
mockArcApi.setup(x => x.azdata).returns(() => fakeAzdataApi);
|
||||
|
||||
sinon.stub(vscode.extensions, 'getExtension').returns(mockArcExtension.object);
|
||||
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }, 'mypassword');
|
||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||
const controllerNode = treeDataProvider.getControllerNode(controllerModel);
|
||||
const children = await treeDataProvider.getChildren(controllerNode);
|
||||
should(children.filter(c => c.label === fakeAzdataApi.postgresInstances[0].name).length).equal(1, 'Should have a Postgres child');
|
||||
should(children.filter(c => c.label === fakeAzdataApi.miaaInstances[0].name).length).equal(1, 'Should have a MIAA child');
|
||||
should(children.length).equal(2, 'Should have excatly 2 children');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeController', function (): void {
|
||||
@@ -104,4 +130,31 @@ describe('AzureArcTreeDataProvider tests', function (): void {
|
||||
should((await treeDataProvider.getChildren()).length).equal(0, 'Removing other node again should do nothing');
|
||||
});
|
||||
});
|
||||
|
||||
describe('openResourceDashboard', function (): void {
|
||||
it('Opening dashboard for nonexistent controller node throws', async function (): Promise<void> {
|
||||
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
|
||||
const openDashboardPromise = treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, '');
|
||||
await should(openDashboardPromise).be.rejected();
|
||||
});
|
||||
|
||||
it('Opening dashboard for nonexistent resource throws', async function (): Promise<void> {
|
||||
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
|
||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||
const openDashboardPromise = treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, '');
|
||||
await should(openDashboardPromise).be.rejected();
|
||||
});
|
||||
|
||||
it('Opening dashboard for existing resource node succeeds', async function (): Promise<void> {
|
||||
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
|
||||
const miaaModel = new MiaaModel(controllerModel, { name: 'miaa-1', resourceType: ResourceType.sqlManagedInstances }, undefined!, treeDataProvider);
|
||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||
const controllerNode = treeDataProvider.getControllerNode(controllerModel)!;
|
||||
const resourceNode = new MiaaTreeNode(miaaModel, controllerModel);
|
||||
sinon.stub(controllerNode, 'getResourceNode').returns(resourceNode);
|
||||
const showDashboardStub = sinon.stub(resourceNode, 'openDashboard');
|
||||
await treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, '');
|
||||
should(showDashboardStub.calledOnce).be.true('showDashboard should have been called exactly once');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
5
extensions/arc/src/typings/arc.d.ts
vendored
5
extensions/arc/src/typings/arc.d.ts
vendored
@@ -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,
|
||||
|
||||
@@ -7,8 +7,8 @@ import { ResourceType } from 'arc';
|
||||
import * as azdata from 'azdata';
|
||||
import * as azurecore from 'azurecore';
|
||||
import * as vscode from 'vscode';
|
||||
import { getConnectionModeDisplayText, getResourceTypeIcon, parseInstanceName, resourceTypeToDisplayName } from '../../../common/utils';
|
||||
import { cssStyles, Endpoints, IconPathHelper, iconSize } from '../../../constants';
|
||||
import { getConnectionModeDisplayText, getResourceTypeIcon, resourceTypeToDisplayName } from '../../../common/utils';
|
||||
import { cssStyles, Endpoints, IconPathHelper, controllerTroubleshootDocsUrl, iconSize } from '../../../constants';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { ControllerModel } from '../../../models/controllerModel';
|
||||
import { DashboardPage } from '../../components/dashboardPage';
|
||||
@@ -93,7 +93,7 @@ export class ControllerDashboardOverviewPage extends DashboardPage {
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
}, {
|
||||
displayName: loc.compute,
|
||||
displayName: loc.state,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '34%',
|
||||
isReadOnly: true,
|
||||
@@ -178,18 +178,30 @@ export class ControllerDashboardOverviewPage extends DashboardPage {
|
||||
this._openInAzurePortalButton.onDidClick(async () => {
|
||||
const config = this._controllerModel.controllerConfig;
|
||||
if (config) {
|
||||
vscode.env.openExternal(vscode.Uri.parse(
|
||||
await vscode.env.openExternal(vscode.Uri.parse(
|
||||
`https://portal.azure.com/#resource/subscriptions/${config.spec.settings.azure.subscription}/resourceGroups/${config.spec.settings.azure.resourceGroup}/providers/Microsoft.AzureData/${ResourceType.dataControllers}/${config.metadata.name}`));
|
||||
} else {
|
||||
vscode.window.showErrorMessage(loc.couldNotFindControllerRegistration);
|
||||
}
|
||||
}));
|
||||
|
||||
const troubleshootButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
|
||||
label: loc.troubleshoot,
|
||||
iconPath: IconPathHelper.wrench
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
troubleshootButton.onDidClick(async () => {
|
||||
await vscode.env.openExternal(vscode.Uri.parse(controllerTroubleshootDocsUrl));
|
||||
})
|
||||
);
|
||||
|
||||
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems(
|
||||
[
|
||||
{ component: newInstance },
|
||||
{ component: refreshButton, toolbarSeparatorAfter: true },
|
||||
{ component: this._openInAzurePortalButton }
|
||||
{ component: this._openInAzurePortalButton, toolbarSeparatorAfter: true },
|
||||
{ component: troubleshootButton }
|
||||
]
|
||||
).component();
|
||||
}
|
||||
@@ -219,26 +231,18 @@ export class ControllerDashboardOverviewPage extends DashboardPage {
|
||||
iconHeight: iconSize,
|
||||
iconWidth: iconSize
|
||||
}).component();
|
||||
let nameComponent: azdata.Component;
|
||||
if (r.instanceType === ResourceType.postgresInstances) {
|
||||
nameComponent = this.modelView.modelBuilder.text()
|
||||
.withProperties<azdata.TextComponentProperties>({
|
||||
value: r.instanceName || '',
|
||||
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component();
|
||||
} else {
|
||||
nameComponent = this.modelView.modelBuilder.hyperlink()
|
||||
|
||||
const nameComponent = this.modelView.modelBuilder.hyperlink()
|
||||
.withProperties<azdata.HyperlinkComponentProperties>({
|
||||
label: r.instanceName || '',
|
||||
url: ''
|
||||
}).component();
|
||||
(<azdata.HyperlinkComponent>nameComponent).onDidClick(async () => {
|
||||
await this._controllerModel.treeDataProvider.openResourceDashboard(this._controllerModel, r.instanceType || '', parseInstanceName(r.instanceName));
|
||||
});
|
||||
}
|
||||
|
||||
// TODO chgagnon
|
||||
return [imageComponent, nameComponent, resourceTypeToDisplayName(r.instanceType), '-'/* loc.numVCores(r.vCores) */];
|
||||
this.disposables.push(nameComponent.onDidClick(async () => {
|
||||
await this._controllerModel.treeDataProvider.openResourceDashboard(this._controllerModel, r.instanceType || '', r.instanceName);
|
||||
}));
|
||||
|
||||
return [imageComponent, nameComponent, resourceTypeToDisplayName(r.instanceType), r.state];
|
||||
});
|
||||
this._arcResourcesLoadingComponent.loading = !this._controllerModel.registrationsLastUpdated;
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import * as azdata from 'azdata';
|
||||
import * as azdataExt from 'azdata-ext';
|
||||
import * as azurecore from 'azurecore';
|
||||
import * as vscode from 'vscode';
|
||||
import { getDatabaseStateDisplayText, promptForResourceDeletion } from '../../../common/utils';
|
||||
import { cssStyles, Endpoints, IconPathHelper } from '../../../constants';
|
||||
import { getDatabaseStateDisplayText, promptForInstanceDeletion } from '../../../common/utils';
|
||||
import { cssStyles, IconPathHelper, miaaTroubleshootDocsUrl } from '../../../constants';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { ControllerModel } from '../../../models/controllerModel';
|
||||
import { MiaaModel } from '../../../models/miaaModel';
|
||||
@@ -198,13 +198,22 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
|
||||
deleteButton.onDidClick(async () => {
|
||||
deleteButton.enabled = false;
|
||||
try {
|
||||
if (await promptForResourceDeletion(this._miaaModel.info.name)) {
|
||||
await this._azdataApi.azdata.arc.sql.mi.delete(this._miaaModel.info.name);
|
||||
if (await promptForInstanceDeletion(this._miaaModel.info.name)) {
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: loc.deletingInstance(this._miaaModel.info.name),
|
||||
cancellable: false
|
||||
},
|
||||
(_progress, _token) => {
|
||||
return this._azdataApi.azdata.arc.sql.mi.delete(this._miaaModel.info.name);
|
||||
}
|
||||
);
|
||||
await this._controllerModel.refreshTreeNode();
|
||||
vscode.window.showInformationMessage(loc.resourceDeleted(this._miaaModel.info.name));
|
||||
vscode.window.showInformationMessage(loc.instanceDeleted(this._miaaModel.info.name));
|
||||
}
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.resourceDeletionFailed(this._miaaModel.info.name, error));
|
||||
vscode.window.showErrorMessage(loc.instanceDeletionFailed(this._miaaModel.info.name, error));
|
||||
} finally {
|
||||
deleteButton.enabled = true;
|
||||
}
|
||||
@@ -248,6 +257,17 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
|
||||
}
|
||||
}));
|
||||
|
||||
const troubleshootButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
|
||||
label: loc.troubleshoot,
|
||||
iconPath: IconPathHelper.wrench
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
troubleshootButton.onDidClick(async () => {
|
||||
await vscode.env.openExternal(vscode.Uri.parse(miaaTroubleshootDocsUrl));
|
||||
})
|
||||
);
|
||||
|
||||
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems(
|
||||
[
|
||||
{ component: deleteButton },
|
||||
@@ -332,19 +352,13 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
|
||||
}
|
||||
|
||||
private refreshDashboardLinks(): void {
|
||||
const kibanaEndpoint = this._controllerModel.getEndpoint(Endpoints.logsui);
|
||||
if (kibanaEndpoint && this._miaaModel.config) {
|
||||
const kibanaQuery = `kubernetes_namespace:"${this._miaaModel.config.metadata.namespace}" and custom_resource_name :"${this._miaaModel.config.metadata.name}"`;
|
||||
const kibanaUrl = `${kibanaEndpoint.endpoint}/app/kibana#/discover?_a=(query:(language:kuery,query:'${kibanaQuery}'))`;
|
||||
if (this._miaaModel.config) {
|
||||
const kibanaUrl = this._miaaModel.config.status.logSearchDashboard ?? '';
|
||||
this._kibanaLink.label = kibanaUrl;
|
||||
this._kibanaLink.url = kibanaUrl;
|
||||
this._kibanaLoading!.loading = false;
|
||||
}
|
||||
|
||||
const grafanaEndpoint = this._controllerModel.getEndpoint(Endpoints.metricsui);
|
||||
if (grafanaEndpoint && this._miaaModel.config) {
|
||||
const grafanaQuery = `var-hostname=${this._miaaModel.info.name}-0`;
|
||||
const grafanaUrl = grafanaEndpoint ? `${grafanaEndpoint.endpoint}/d/40q72HnGk/sql-managed-instance-metrics?${grafanaQuery}` : '';
|
||||
const grafanaUrl = this._miaaModel.config.status.metricsDashboard ?? '';
|
||||
this._grafanaLink.label = grafanaUrl;
|
||||
this._grafanaLink.url = grafanaUrl;
|
||||
this._grafanaLoading!.loading = false;
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { IconPathHelper } from '../../../constants';
|
||||
import { DashboardPage } from '../../components/dashboardPage';
|
||||
|
||||
export class PostgresBackupPage extends DashboardPage {
|
||||
protected get title(): string {
|
||||
return loc.backup;
|
||||
}
|
||||
|
||||
protected get id(): string {
|
||||
return 'postgres-backup';
|
||||
}
|
||||
|
||||
protected get icon(): { dark: string; light: string; } {
|
||||
return IconPathHelper.backup;
|
||||
}
|
||||
|
||||
protected get container(): azdata.Component {
|
||||
return this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: loc.backup }).component();
|
||||
}
|
||||
|
||||
protected get toolbarContainer(): azdata.ToolbarContainer {
|
||||
return this.modelView.modelBuilder.toolbarContainer().component();
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { IconPathHelper } from '../../../constants';
|
||||
import { DashboardPage } from '../../components/dashboardPage';
|
||||
|
||||
export class PostgresComputeStoragePage extends DashboardPage {
|
||||
protected get title(): string {
|
||||
return loc.computeAndStorage;
|
||||
}
|
||||
|
||||
protected get id(): string {
|
||||
return 'postgres-compute-storage';
|
||||
}
|
||||
|
||||
protected get icon(): { dark: string; light: string; } {
|
||||
return IconPathHelper.computeStorage;
|
||||
}
|
||||
|
||||
protected get container(): azdata.Component {
|
||||
return this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: loc.computeAndStorage }).component();
|
||||
}
|
||||
|
||||
protected get toolbarContainer(): azdata.ToolbarContainer {
|
||||
return this.modelView.modelBuilder.toolbarContainer().component();
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as azdata from 'azdata';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { IconPathHelper, cssStyles } from '../../../constants';
|
||||
@@ -12,13 +11,12 @@ import { DashboardPage } from '../../components/dashboardPage';
|
||||
import { PostgresModel } from '../../../models/postgresModel';
|
||||
|
||||
export class PostgresConnectionStringsPage extends DashboardPage {
|
||||
private loading?: azdata.LoadingComponent;
|
||||
private keyValueContainer?: KeyValueContainer;
|
||||
|
||||
constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) {
|
||||
super(modelView);
|
||||
|
||||
this.disposables.push(this._postgresModel.onServiceUpdated(
|
||||
this.disposables.push(this._postgresModel.onConfigUpdated(
|
||||
() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())));
|
||||
}
|
||||
|
||||
@@ -61,44 +59,20 @@ export class PostgresConnectionStringsPage extends DashboardPage {
|
||||
|
||||
this.keyValueContainer = new KeyValueContainer(this.modelView.modelBuilder, this.getConnectionStrings());
|
||||
this.disposables.push(this.keyValueContainer);
|
||||
|
||||
this.loading = this.modelView.modelBuilder.loadingComponent()
|
||||
.withItem(this.keyValueContainer.container)
|
||||
.withProperties<azdata.LoadingComponentProperties>({
|
||||
loading: !this._postgresModel.serviceLastUpdated
|
||||
}).component();
|
||||
|
||||
content.addItem(this.loading);
|
||||
content.addItem(this.keyValueContainer.container);
|
||||
this.initialized = true;
|
||||
return root;
|
||||
}
|
||||
|
||||
protected get toolbarContainer(): azdata.ToolbarContainer {
|
||||
const refreshButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
|
||||
label: loc.refresh,
|
||||
iconPath: IconPathHelper.refresh
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
refreshButton.onDidClick(async () => {
|
||||
refreshButton.enabled = false;
|
||||
try {
|
||||
this.loading!.loading = true;
|
||||
await this._postgresModel.refresh();
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.refreshFailed(error));
|
||||
} finally {
|
||||
refreshButton.enabled = true;
|
||||
}
|
||||
}));
|
||||
|
||||
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([
|
||||
{ component: refreshButton }
|
||||
]).component();
|
||||
return this.modelView.modelBuilder.toolbarContainer().component();
|
||||
}
|
||||
|
||||
private getConnectionStrings(): KeyValue[] {
|
||||
const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint;
|
||||
const endpoint = this._postgresModel.endpoint;
|
||||
if (!endpoint) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new InputKeyValue(this.modelView.modelBuilder, 'ADO.NET', `Server=${endpoint.ip};Database=postgres;Port=${endpoint.port};User Id=postgres;Password={your_password_here};Ssl Mode=Require;`),
|
||||
@@ -115,6 +89,5 @@ export class PostgresConnectionStringsPage extends DashboardPage {
|
||||
|
||||
private handleServiceUpdated() {
|
||||
this.keyValueContainer?.refresh(this.getConnectionStrings());
|
||||
this.loading!.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,15 +10,13 @@ import { ControllerModel } from '../../../models/controllerModel';
|
||||
import { PostgresModel } from '../../../models/postgresModel';
|
||||
import { PostgresOverviewPage } from './postgresOverviewPage';
|
||||
import { PostgresConnectionStringsPage } from './postgresConnectionStringsPage';
|
||||
import { PostgresPropertiesPage } from './postgresPropertiesPage';
|
||||
import { Dashboard } from '../../components/dashboard';
|
||||
import { PostgresDiagnoseAndSolveProblemsPage } from './postgresDiagnoseAndSolveProblemsPage';
|
||||
import { PostgresSupportRequestPage } from './postgresSupportRequestPage';
|
||||
import { PostgresResourceHealthPage } from './postgresResourceHealthPage';
|
||||
|
||||
export class PostgresDashboard extends Dashboard {
|
||||
constructor(private _context: vscode.ExtensionContext, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
|
||||
super(loc.postgresDashboard(_postgresModel.name), 'ArcPgDashboard');
|
||||
super(loc.postgresDashboard(_postgresModel.info.name), 'ArcPgDashboard');
|
||||
}
|
||||
|
||||
public async showDashboard(): Promise<void> {
|
||||
@@ -32,24 +30,22 @@ export class PostgresDashboard extends Dashboard {
|
||||
protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> {
|
||||
const overviewPage = new PostgresOverviewPage(modelView, this._controllerModel, this._postgresModel);
|
||||
const connectionStringsPage = new PostgresConnectionStringsPage(modelView, this._postgresModel);
|
||||
const propertiesPage = new PostgresPropertiesPage(modelView, this._controllerModel, this._postgresModel);
|
||||
const resourceHealthPage = new PostgresResourceHealthPage(modelView, this._postgresModel);
|
||||
// TODO: Removed properties page while investigating bug where refreshed values don't appear in UI
|
||||
// const propertiesPage = new PostgresPropertiesPage(modelView, this._controllerModel, this._postgresModel);
|
||||
const diagnoseAndSolveProblemsPage = new PostgresDiagnoseAndSolveProblemsPage(modelView, this._context, this._postgresModel);
|
||||
const supportRequestPage = new PostgresSupportRequestPage(modelView);
|
||||
const supportRequestPage = new PostgresSupportRequestPage(modelView, this._controllerModel, this._postgresModel);
|
||||
|
||||
return [
|
||||
overviewPage.tab,
|
||||
{
|
||||
title: loc.settings,
|
||||
tabs: [
|
||||
connectionStringsPage.tab,
|
||||
propertiesPage.tab
|
||||
connectionStringsPage.tab
|
||||
]
|
||||
},
|
||||
{
|
||||
title: loc.supportAndTroubleshooting,
|
||||
tabs: [
|
||||
resourceHealthPage.tab,
|
||||
diagnoseAndSolveProblemsPage.tab,
|
||||
supportRequestPage.tab
|
||||
]
|
||||
|
||||
@@ -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');
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { IconPathHelper } from '../../../constants';
|
||||
import { DashboardPage } from '../../components/dashboardPage';
|
||||
|
||||
export class PostgresNetworkingPage extends DashboardPage {
|
||||
protected get title(): string {
|
||||
return loc.networking;
|
||||
}
|
||||
|
||||
protected get id(): string {
|
||||
return 'postgres-networking';
|
||||
}
|
||||
|
||||
protected get icon(): { dark: string; light: string; } {
|
||||
return IconPathHelper.networking;
|
||||
}
|
||||
|
||||
protected get container(): azdata.Component {
|
||||
return this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: loc.networking }).component();
|
||||
}
|
||||
|
||||
protected get toolbarContainer(): azdata.ToolbarContainer {
|
||||
return this.modelView.modelBuilder.toolbarContainer().component();
|
||||
}
|
||||
}
|
||||
@@ -5,33 +5,34 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as azdata from 'azdata';
|
||||
import * as azdataExt from 'azdata-ext';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { IconPathHelper, cssStyles, Endpoints } from '../../../constants';
|
||||
import { IconPathHelper, cssStyles } from '../../../constants';
|
||||
import { DashboardPage } from '../../components/dashboardPage';
|
||||
import { ControllerModel } from '../../../models/controllerModel';
|
||||
import { PostgresModel } from '../../../models/postgresModel';
|
||||
import { promptAndConfirmPassword } from '../../../common/utils';
|
||||
import { promptAndConfirmPassword, promptForInstanceDeletion } from '../../../common/utils';
|
||||
import { ResourceType } from 'arc';
|
||||
|
||||
export class PostgresOverviewPage extends DashboardPage {
|
||||
|
||||
private propertiesLoading?: azdata.LoadingComponent;
|
||||
private kibanaLoading?: azdata.LoadingComponent;
|
||||
private grafanaLoading?: azdata.LoadingComponent;
|
||||
private nodesTableLoading?: azdata.LoadingComponent;
|
||||
private propertiesLoading!: azdata.LoadingComponent;
|
||||
private kibanaLoading!: azdata.LoadingComponent;
|
||||
private grafanaLoading!: azdata.LoadingComponent;
|
||||
|
||||
private properties?: azdata.PropertiesContainerComponent;
|
||||
private kibanaLink?: azdata.HyperlinkComponent;
|
||||
private grafanaLink?: azdata.HyperlinkComponent;
|
||||
private nodesTable?: azdata.DeclarativeTableComponent;
|
||||
private properties!: azdata.PropertiesContainerComponent;
|
||||
private kibanaLink!: azdata.HyperlinkComponent;
|
||||
private grafanaLink!: azdata.HyperlinkComponent;
|
||||
|
||||
private readonly _azdataApi: azdataExt.IExtension;
|
||||
|
||||
constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
|
||||
super(modelView);
|
||||
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
|
||||
|
||||
this.disposables.push(
|
||||
this._controllerModel.onEndpointsUpdated(() => this.eventuallyRunOnInitialized(() => this.handleEndpointsUpdated())),
|
||||
this._controllerModel.onRegistrationsUpdated(() => this.eventuallyRunOnInitialized(() => this.handleRegistrationsUpdated())),
|
||||
this._postgresModel.onServiceUpdated(() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())),
|
||||
this._postgresModel.onPodsUpdated(() => this.eventuallyRunOnInitialized(() => this.handlePodsUpdated())));
|
||||
this._postgresModel.onConfigUpdated(() => this.eventuallyRunOnInitialized(() => this.handleConfigUpdated())));
|
||||
}
|
||||
|
||||
protected get title(): string {
|
||||
@@ -60,7 +61,7 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
this.propertiesLoading = this.modelView.modelBuilder.loadingComponent()
|
||||
.withItem(this.properties)
|
||||
.withProperties<azdata.LoadingComponentProperties>({
|
||||
loading: !this._controllerModel.registrationsLastUpdated && !this._postgresModel.serviceLastUpdated && !this._postgresModel.podsLastUpdated
|
||||
loading: !this._controllerModel.registrationsLastUpdated && !this._postgresModel.configLastUpdated
|
||||
}).component();
|
||||
|
||||
content.addItem(this.propertiesLoading, { CSSStyles: cssStyles.text });
|
||||
@@ -72,29 +73,26 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
CSSStyles: titleCSS
|
||||
}).component());
|
||||
|
||||
this.kibanaLink = this.modelView.modelBuilder.hyperlink()
|
||||
.withProperties<azdata.HyperlinkComponentProperties>({
|
||||
label: this.getKibanaLink(),
|
||||
url: this.getKibanaLink()
|
||||
}).component();
|
||||
this.kibanaLink = this.modelView.modelBuilder.hyperlink().component();
|
||||
|
||||
this.grafanaLink = this.modelView.modelBuilder.hyperlink()
|
||||
.withProperties<azdata.HyperlinkComponentProperties>({
|
||||
label: this.getGrafanaLink(),
|
||||
url: this.getGrafanaLink()
|
||||
}).component();
|
||||
this.grafanaLink = this.modelView.modelBuilder.hyperlink().component();
|
||||
|
||||
this.kibanaLoading = this.modelView.modelBuilder.loadingComponent()
|
||||
.withItem(this.kibanaLink)
|
||||
.withProperties<azdata.LoadingComponentProperties>({
|
||||
loading: !this._controllerModel.endpointsLastUpdated
|
||||
}).component();
|
||||
.withProperties<azdata.LoadingComponentProperties>(
|
||||
{ loading: !this._postgresModel?.configLastUpdated }
|
||||
)
|
||||
.component();
|
||||
|
||||
this.grafanaLoading = this.modelView.modelBuilder.loadingComponent()
|
||||
.withItem(this.grafanaLink)
|
||||
.withProperties<azdata.LoadingComponentProperties>({
|
||||
loading: !this._controllerModel.endpointsLastUpdated
|
||||
}).component();
|
||||
.withProperties<azdata.LoadingComponentProperties>(
|
||||
{ loading: !this._postgresModel?.configLastUpdated }
|
||||
)
|
||||
.component();
|
||||
|
||||
this.refreshDashboardLinks();
|
||||
|
||||
this.kibanaLoading.component = this.kibanaLink;
|
||||
this.grafanaLoading.component = this.grafanaLink;
|
||||
|
||||
const endpointsTable = this.modelView.modelBuilder.declarativeTable().withProperties<azdata.DeclarativeTableProperties>({
|
||||
width: '100%',
|
||||
@@ -134,60 +132,8 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
[loc.kibanaDashboard, this.kibanaLoading, loc.kibanaDashboardDescription],
|
||||
[loc.grafanaDashboard, this.grafanaLoading, loc.grafanaDashboardDescription]]
|
||||
}).component();
|
||||
|
||||
content.addItem(endpointsTable);
|
||||
|
||||
// Server group nodes
|
||||
content.addItem(this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: loc.serverGroupNodes,
|
||||
CSSStyles: titleCSS
|
||||
}).component());
|
||||
|
||||
this.nodesTable = this.modelView.modelBuilder.declarativeTable().withProperties<azdata.DeclarativeTableProperties>({
|
||||
width: '100%',
|
||||
columns: [
|
||||
{
|
||||
displayName: loc.name,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: '30%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
},
|
||||
{
|
||||
displayName: loc.type,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: '15%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
},
|
||||
{
|
||||
displayName: loc.status,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: '20%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
},
|
||||
{
|
||||
displayName: loc.fullyQualifiedDomain,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: '35%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
}
|
||||
],
|
||||
data: this.getNodes()
|
||||
}).component();
|
||||
|
||||
this.nodesTableLoading = this.modelView.modelBuilder.loadingComponent()
|
||||
.withItem(this.nodesTable)
|
||||
.withProperties<azdata.LoadingComponentProperties>({
|
||||
loading: !this._postgresModel.serviceLastUpdated && !this._postgresModel.podsLastUpdated
|
||||
}).component();
|
||||
|
||||
content.addItem(this.nodesTableLoading, { CSSStyles: { 'margin-bottom': '20px' } });
|
||||
this.initialized = true;
|
||||
return root;
|
||||
}
|
||||
@@ -205,11 +151,13 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
try {
|
||||
const password = await promptAndConfirmPassword(input => !input ? loc.enterANonEmptyPassword : '');
|
||||
if (password) {
|
||||
await this._postgresModel.update(s => {
|
||||
// TODO chgagnon
|
||||
// s.arc = s.arc ?? new DuskyObjectModelsDatabaseServiceArcPayload();
|
||||
s.arc.servicePassword = password;
|
||||
});
|
||||
await this._azdataApi.azdata.arc.postgres.server.edit(
|
||||
this._postgresModel.info.name,
|
||||
{
|
||||
adminPassword: true,
|
||||
noWait: true
|
||||
},
|
||||
{ 'AZDATA_PASSWORD': password });
|
||||
vscode.window.showInformationMessage(loc.passwordReset);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -229,15 +177,22 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
deleteButton.onDidClick(async () => {
|
||||
deleteButton.enabled = false;
|
||||
try {
|
||||
/*
|
||||
if (await promptForResourceDeletion(this._postgresModel.namespace, this._postgresModel.name)) {
|
||||
await this._postgresModel.delete();
|
||||
await this._controllerModel.deleteRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name);
|
||||
vscode.window.showInformationMessage(loc.resourceDeleted(this._postgresModel.fullName));
|
||||
if (await promptForInstanceDeletion(this._postgresModel.info.name)) {
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: loc.deletingInstance(this._postgresModel.info.name),
|
||||
cancellable: false
|
||||
},
|
||||
(_progress, _token) => {
|
||||
return this._azdataApi.azdata.arc.postgres.server.delete(this._postgresModel.info.name);
|
||||
}
|
||||
);
|
||||
await this._controllerModel.refreshTreeNode();
|
||||
vscode.window.showInformationMessage(loc.instanceDeleted(this._postgresModel.info.name));
|
||||
}
|
||||
*/
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.resourceDeletionFailed(this._postgresModel.fullName, error));
|
||||
vscode.window.showErrorMessage(loc.instanceDeletionFailed(this._postgresModel.info.name, error));
|
||||
} finally {
|
||||
deleteButton.enabled = true;
|
||||
}
|
||||
@@ -256,7 +211,6 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
this.propertiesLoading!.loading = true;
|
||||
this.kibanaLoading!.loading = true;
|
||||
this.grafanaLoading!.loading = true;
|
||||
this.nodesTableLoading!.loading = true;
|
||||
|
||||
await Promise.all([
|
||||
this._postgresModel.refresh(),
|
||||
@@ -278,15 +232,13 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
|
||||
this.disposables.push(
|
||||
openInAzurePortalButton.onDidClick(async () => {
|
||||
/*
|
||||
const r = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name);
|
||||
if (!r) {
|
||||
vscode.window.showErrorMessage(loc.couldNotFindAzureResource(this._postgresModel.fullName));
|
||||
} else {
|
||||
const azure = this._controllerModel.controllerConfig?.spec.settings.azure;
|
||||
if (azure) {
|
||||
vscode.env.openExternal(vscode.Uri.parse(
|
||||
`https://portal.azure.com/#resource/subscriptions/${r.subscriptionId}/resourceGroups/${r.resourceGroupName}/providers/Microsoft.AzureData/${ResourceType.postgresInstances}/${r.instanceName}`));
|
||||
`https://portal.azure.com/#resource/subscriptions/${azure.subscription}/resourceGroups/${azure.resourceGroup}/providers/Microsoft.AzureData/${ResourceType.postgresInstances}/${this._postgresModel.info.name}`));
|
||||
} else {
|
||||
vscode.window.showErrorMessage(loc.couldNotFindControllerRegistration);
|
||||
}
|
||||
*/
|
||||
}));
|
||||
|
||||
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([
|
||||
@@ -298,63 +250,35 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
}
|
||||
|
||||
private getProperties(): azdata.PropertiesContainerItem[] {
|
||||
/*
|
||||
const registration = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name);
|
||||
const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint;
|
||||
const status = this._postgresModel.config?.status;
|
||||
const azure = this._controllerModel.controllerConfig?.spec.settings.azure;
|
||||
|
||||
return [
|
||||
{ displayName: loc.name, value: this._postgresModel.name },
|
||||
{ displayName: loc.coordinatorEndpoint, value: `postgresql://postgres@${endpoint.ip}:${endpoint.port}` },
|
||||
{ displayName: loc.status, value: this._postgresModel.service?.status?.state ?? '' },
|
||||
{ displayName: loc.resourceGroup, value: azure?.resourceGroup || '-' },
|
||||
{ displayName: loc.dataController, value: this._controllerModel.controllerConfig?.metadata.name || '-' },
|
||||
{ displayName: loc.region, value: azure?.location || '-' },
|
||||
{ displayName: loc.namespace, value: this._postgresModel.config?.metadata.namespace || '-' },
|
||||
{ displayName: loc.subscriptionId, value: azure?.subscription || '-' },
|
||||
{ displayName: loc.externalEndpoint, value: this._postgresModel.config?.status.externalEndpoint || '-' },
|
||||
{ displayName: loc.status, value: status ? `${status.state} (${status.readyPods} ${loc.podsReady})` : '-' },
|
||||
{ displayName: loc.postgresAdminUsername, value: 'postgres' },
|
||||
{ displayName: loc.dataController, value: this._controllerModel?.namespace ?? '' },
|
||||
{ displayName: loc.nodeConfiguration, value: this._postgresModel.configuration },
|
||||
{ displayName: loc.subscriptionId, value: registration?.subscriptionId ?? '' },
|
||||
{ displayName: loc.postgresVersion, value: this._postgresModel.service?.spec?.engine?.version?.toString() ?? '' }
|
||||
{ displayName: loc.postgresVersion, value: this._postgresModel.engineVersion ?? '-' },
|
||||
{ displayName: loc.nodeConfiguration, value: this._postgresModel.scaleConfiguration || '-' }
|
||||
];
|
||||
*/
|
||||
return [];
|
||||
}
|
||||
|
||||
private getKibanaLink(): string {
|
||||
const kibanaQuery = `kubernetes_namespace:"${this._postgresModel.namespace}" and custom_resource_name:"${this._postgresModel.name}"`;
|
||||
return `${this._controllerModel.getEndpoint(Endpoints.logsui)?.endpoint}/app/kibana#/discover?_a=(query:(language:kuery,query:'${kibanaQuery}'))`;
|
||||
private refreshDashboardLinks(): void {
|
||||
if (this._postgresModel.config) {
|
||||
const kibanaUrl = this._postgresModel.config.status.logSearchDashboard ?? '';
|
||||
this.kibanaLink.label = kibanaUrl;
|
||||
this.kibanaLink.url = kibanaUrl;
|
||||
this.kibanaLoading.loading = false;
|
||||
|
||||
const grafanaUrl = this._postgresModel.config.status.metricsDashboard ?? '';
|
||||
this.grafanaLink.label = grafanaUrl;
|
||||
this.grafanaLink.url = grafanaUrl;
|
||||
this.grafanaLoading.loading = false;
|
||||
}
|
||||
|
||||
private getGrafanaLink(): string {
|
||||
const grafanaQuery = `var-Namespace=${this._postgresModel.namespace}&var-Name=${this._postgresModel.name}`;
|
||||
return `${this._controllerModel.getEndpoint(Endpoints.metricsui)?.endpoint}/d/postgres-metrics?${grafanaQuery}`;
|
||||
}
|
||||
|
||||
private getNodes(): string[][] {
|
||||
/* TODO chgagnon
|
||||
const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint;
|
||||
return this._postgresModel.pods?.map((pod: V1Pod) => {
|
||||
const name = pod.metadata?.name ?? '';
|
||||
const role: PodRole | undefined = PostgresModel.getPodRole(pod);
|
||||
const service = pod.metadata?.annotations?.['arcdata.microsoft.com/serviceHost'];
|
||||
const internalDns = service ? `${name}.${service}` : '';
|
||||
|
||||
return [
|
||||
name,
|
||||
PostgresModel.getPodRoleName(role),
|
||||
PostgresModel.getPodStatus(pod),
|
||||
role === PodRole.Router ? `${endpoint.ip}:${endpoint.port}` : internalDns
|
||||
];
|
||||
}) ?? [];
|
||||
*/
|
||||
return [];
|
||||
}
|
||||
|
||||
private handleEndpointsUpdated() {
|
||||
this.kibanaLink!.label = this.getKibanaLink();
|
||||
this.kibanaLink!.url = this.getKibanaLink();
|
||||
this.kibanaLoading!.loading = false;
|
||||
|
||||
this.grafanaLink!.label = this.getGrafanaLink();
|
||||
this.grafanaLink!.url = this.getGrafanaLink();
|
||||
this.grafanaLoading!.loading = false;
|
||||
}
|
||||
|
||||
private handleRegistrationsUpdated() {
|
||||
@@ -362,19 +286,9 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
this.propertiesLoading!.loading = false;
|
||||
}
|
||||
|
||||
private handleServiceUpdated() {
|
||||
private handleConfigUpdated() {
|
||||
this.properties!.propertyItems = this.getProperties();
|
||||
this.propertiesLoading!.loading = false;
|
||||
|
||||
this.nodesTable!.data = this.getNodes();
|
||||
this.nodesTableLoading!.loading = false;
|
||||
}
|
||||
|
||||
private handlePodsUpdated() {
|
||||
this.properties!.propertyItems = this.getProperties();
|
||||
this.propertiesLoading!.loading = false;
|
||||
|
||||
this.nodesTable!.data = this.getNodes();
|
||||
this.nodesTableLoading!.loading = false;
|
||||
this.refreshDashboardLinks();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as vscode from 'vscode';
|
||||
import * as azdata from 'azdata';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { IconPathHelper, cssStyles } from '../../../constants';
|
||||
import { KeyValueContainer, KeyValue } from '../../components/keyValueContainer';
|
||||
import { KeyValueContainer, KeyValue, InputKeyValue, TextKeyValue } from '../../components/keyValueContainer';
|
||||
import { DashboardPage } from '../../components/dashboardPage';
|
||||
import { ControllerModel } from '../../../models/controllerModel';
|
||||
import { PostgresModel } from '../../../models/postgresModel';
|
||||
@@ -19,7 +19,7 @@ export class PostgresPropertiesPage extends DashboardPage {
|
||||
constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
|
||||
super(modelView);
|
||||
|
||||
this.disposables.push(this._postgresModel.onServiceUpdated(
|
||||
this.disposables.push(this._postgresModel.onConfigUpdated(
|
||||
() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())));
|
||||
|
||||
this.disposables.push(this._controllerModel.onRegistrationsUpdated(
|
||||
@@ -54,7 +54,7 @@ export class PostgresPropertiesPage extends DashboardPage {
|
||||
this.loading = this.modelView.modelBuilder.loadingComponent()
|
||||
.withItem(this.keyValueContainer.container)
|
||||
.withProperties<azdata.LoadingComponentProperties>({
|
||||
loading: !this._postgresModel.serviceLastUpdated && !this._controllerModel.registrationsLastUpdated
|
||||
loading: !this._postgresModel.configLastUpdated && !this._controllerModel.registrationsLastUpdated
|
||||
}).component();
|
||||
|
||||
content.addItem(this.loading);
|
||||
@@ -91,24 +91,20 @@ export class PostgresPropertiesPage extends DashboardPage {
|
||||
}
|
||||
|
||||
private getProperties(): KeyValue[] {
|
||||
/*
|
||||
const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint;
|
||||
const connectionString = `postgresql://postgres@${endpoint.ip}:${endpoint.port}`;
|
||||
const registration = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name);
|
||||
const endpoint = this._postgresModel.endpoint;
|
||||
const status = this._postgresModel.config?.status;
|
||||
|
||||
return [
|
||||
new InputKeyValue(this.modelView.modelBuilder, loc.coordinatorEndpoint, connectionString),
|
||||
new InputKeyValue(this.modelView.modelBuilder, loc.coordinatorEndpoint, endpoint ? `postgresql://postgres@${endpoint.ip}:${endpoint.port}` : ''),
|
||||
new InputKeyValue(this.modelView.modelBuilder, loc.postgresAdminUsername, 'postgres'),
|
||||
new TextKeyValue(this.modelView.modelBuilder, loc.status, this._postgresModel.service?.status?.state ?? 'Unknown'),
|
||||
new TextKeyValue(this.modelView.modelBuilder, loc.status, status ? `${status.state} (${status.readyPods} ${loc.podsReady})` : loc.unknown),
|
||||
// TODO: Make this a LinkKeyValue that opens the controller dashboard
|
||||
new TextKeyValue(this.modelView.modelBuilder, loc.dataController, this._controllerModel.namespace ?? ''),
|
||||
new TextKeyValue(this.modelView.modelBuilder, loc.nodeConfiguration, this._postgresModel.configuration),
|
||||
new TextKeyValue(this.modelView.modelBuilder, loc.postgresVersion, this._postgresModel.service?.spec?.engine?.version?.toString() ?? ''),
|
||||
new TextKeyValue(this.modelView.modelBuilder, loc.resourceGroup, registration?.resourceGroupName ?? ''),
|
||||
new TextKeyValue(this.modelView.modelBuilder, loc.subscriptionId, registration?.subscriptionId ?? '')
|
||||
new TextKeyValue(this.modelView.modelBuilder, loc.dataController, this._controllerModel.controllerConfig?.metadata.namespace ?? ''),
|
||||
new TextKeyValue(this.modelView.modelBuilder, loc.nodeConfiguration, this._postgresModel.scaleConfiguration ?? ''),
|
||||
new TextKeyValue(this.modelView.modelBuilder, loc.postgresVersion, this._postgresModel.engineVersion ?? ''),
|
||||
new TextKeyValue(this.modelView.modelBuilder, loc.resourceGroup, this._controllerModel.controllerConfig?.spec.settings.azure.resourceGroup ?? ''),
|
||||
new TextKeyValue(this.modelView.modelBuilder, loc.subscriptionId, this._controllerModel.controllerConfig?.spec.settings.azure.subscription ?? '')
|
||||
];
|
||||
*/
|
||||
return [];
|
||||
}
|
||||
|
||||
private handleRegistrationsUpdated() {
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as azdata from 'azdata';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { IconPathHelper, cssStyles } from '../../../constants';
|
||||
import { DashboardPage } from '../../components/dashboardPage';
|
||||
import { PostgresModel } from '../../../models/postgresModel';
|
||||
import { fromNow } from '../../../common/date';
|
||||
|
||||
export class PostgresResourceHealthPage extends DashboardPage {
|
||||
private interval: NodeJS.Timeout;
|
||||
private podsUpdated?: azdata.TextComponent;
|
||||
private podsLoading?: azdata.LoadingComponent;
|
||||
private conditionsLoading?: azdata.LoadingComponent;
|
||||
private podsTable?: azdata.DeclarativeTableComponent;
|
||||
private conditionsTable?: azdata.DeclarativeTableComponent;
|
||||
|
||||
constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) {
|
||||
super(modelView);
|
||||
|
||||
this.disposables.push(
|
||||
modelView.onClosed(() => {
|
||||
try { clearInterval(this.interval); }
|
||||
catch { }
|
||||
}));
|
||||
|
||||
this.disposables.push(this._postgresModel.onServiceUpdated(
|
||||
() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())));
|
||||
|
||||
// Keep the last updated timestamps up to date with the current time
|
||||
this.interval = setInterval(() => this.handleServiceUpdated(), 60 * 1000);
|
||||
}
|
||||
|
||||
protected get title(): string {
|
||||
return loc.resourceHealth;
|
||||
}
|
||||
|
||||
protected get id(): string {
|
||||
return 'postgres-resource-health';
|
||||
}
|
||||
|
||||
protected get icon(): { dark: string; light: string; } {
|
||||
return IconPathHelper.health;
|
||||
}
|
||||
|
||||
protected get container(): azdata.Component {
|
||||
const root = this.modelView.modelBuilder.divContainer().component();
|
||||
const content = this.modelView.modelBuilder.divContainer().component();
|
||||
root.addItem(content, { CSSStyles: { 'margin': '20px' } });
|
||||
|
||||
content.addItem(this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: loc.resourceHealth,
|
||||
CSSStyles: { ...cssStyles.title, 'margin-bottom': '30px' }
|
||||
}).component());
|
||||
|
||||
content.addItem(this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: loc.podOverview,
|
||||
CSSStyles: { ...cssStyles.title, 'margin-block-end': '0' }
|
||||
}).component());
|
||||
|
||||
this.podsUpdated = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: this.getPodsLastUpdated(),
|
||||
CSSStyles: { ...cssStyles.text, 'font-size': '12px', 'margin-block-start': '0' }
|
||||
}).component();
|
||||
|
||||
content.addItem(this.podsUpdated);
|
||||
|
||||
// Pod overview
|
||||
this.podsTable = this.modelView.modelBuilder.declarativeTable().withProperties<azdata.DeclarativeTableProperties>({
|
||||
columns: [
|
||||
{
|
||||
displayName: '',
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '35%',
|
||||
isReadOnly: true,
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: { ...cssStyles.tableRow, 'font-size': '20px', 'font-weight': 'bold', 'padding': '7px' }
|
||||
},
|
||||
{
|
||||
displayName: '',
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '65%',
|
||||
isReadOnly: true,
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: { ...cssStyles.tableRow, 'padding': '7px' }
|
||||
}
|
||||
],
|
||||
data: this.getPodsTable()
|
||||
}).component();
|
||||
|
||||
this.podsLoading = this.modelView.modelBuilder.loadingComponent()
|
||||
.withItem(this.podsTable)
|
||||
.withProperties<azdata.LoadingComponentProperties>({
|
||||
loading: !this._postgresModel.serviceLastUpdated
|
||||
}).component();
|
||||
|
||||
content.addItem(this.podsLoading, { CSSStyles: { 'margin-bottom': '30px' } });
|
||||
|
||||
// Conditions table
|
||||
this.conditionsTable = this.modelView.modelBuilder.declarativeTable().withProperties<azdata.DeclarativeTableProperties>({
|
||||
width: '100%',
|
||||
columns: [
|
||||
{
|
||||
displayName: loc.condition,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '15%',
|
||||
isReadOnly: true,
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
},
|
||||
{
|
||||
displayName: '',
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
width: '1%',
|
||||
isReadOnly: true,
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
},
|
||||
{
|
||||
displayName: loc.details,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '64%',
|
||||
isReadOnly: true,
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
},
|
||||
{
|
||||
displayName: loc.lastUpdated,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '20%',
|
||||
isReadOnly: true,
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: { ...cssStyles.tableRow, 'white-space': 'nowrap' }
|
||||
}
|
||||
],
|
||||
data: this.getConditionsTable()
|
||||
}).component();
|
||||
|
||||
this.conditionsLoading = this.modelView.modelBuilder.loadingComponent()
|
||||
.withItem(this.conditionsTable)
|
||||
.withProperties<azdata.LoadingComponentProperties>({
|
||||
loading: !this._postgresModel.serviceLastUpdated
|
||||
}).component();
|
||||
|
||||
content.addItem(this.conditionsLoading);
|
||||
this.initialized = true;
|
||||
return root;
|
||||
}
|
||||
|
||||
protected get toolbarContainer(): azdata.ToolbarContainer {
|
||||
const refreshButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
|
||||
label: loc.refresh,
|
||||
iconPath: IconPathHelper.refresh
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
refreshButton.onDidClick(async () => {
|
||||
refreshButton.enabled = false;
|
||||
try {
|
||||
this.podsLoading!.loading = true;
|
||||
this.conditionsLoading!.loading = true;
|
||||
await this._postgresModel.refresh();
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.refreshFailed(error));
|
||||
} finally {
|
||||
refreshButton.enabled = true;
|
||||
}
|
||||
}));
|
||||
|
||||
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([
|
||||
{ component: refreshButton }
|
||||
]).component();
|
||||
}
|
||||
|
||||
private getPodsLastUpdated(): string {
|
||||
return this._postgresModel.serviceLastUpdated
|
||||
? loc.updated(fromNow(this._postgresModel.serviceLastUpdated!, true)) : '';
|
||||
}
|
||||
|
||||
private getPodsTable(): (string | number)[][] {
|
||||
return [
|
||||
[this._postgresModel.service?.status?.podsRunning ?? 0, loc.running],
|
||||
[this._postgresModel.service?.status?.podsPending ?? 0, loc.pending],
|
||||
[this._postgresModel.service?.status?.podsFailed ?? 0, loc.failed],
|
||||
[this._postgresModel.service?.status?.podsUnknown ?? 0, loc.unknown]
|
||||
];
|
||||
}
|
||||
|
||||
private getConditionsTable(): (string | azdata.ImageComponent)[][] {
|
||||
/* TODO chgagnon
|
||||
return this._postgresModel.service?.status?.conditions?.map(c => {
|
||||
const healthy = c.type === 'Ready' ? c.status === 'True' : c.status === 'False';
|
||||
|
||||
const image = this.modelView.modelBuilder.image().withProperties<azdata.ImageComponentProperties>({
|
||||
iconPath: healthy ? IconPathHelper.success : IconPathHelper.fail,
|
||||
iconHeight: '20px',
|
||||
iconWidth: '20px',
|
||||
width: '20px',
|
||||
height: '20px'
|
||||
}).component();
|
||||
|
||||
return [
|
||||
c.type ?? '',
|
||||
image,
|
||||
c.message ?? '',
|
||||
c.lastTransitionTime ? fromNow(c.lastTransitionTime!, true) : ''
|
||||
];
|
||||
}) ?? [];
|
||||
*/
|
||||
return [];
|
||||
}
|
||||
|
||||
private handleServiceUpdated() {
|
||||
this.podsUpdated!.value = this.getPodsLastUpdated();
|
||||
this.podsTable!.data = this.getPodsTable();
|
||||
this.podsLoading!.loading = false;
|
||||
|
||||
this.conditionsTable!.data = this.getConditionsTable();
|
||||
this.conditionsLoading!.loading = false;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ControllerInfo } from 'arc';
|
||||
import { ControllerInfo, ResourceInfo } from 'arc';
|
||||
import * as azdata from 'azdata';
|
||||
import * as azdataExt from 'azdata-ext';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
@@ -74,6 +74,7 @@ abstract class ControllerDialogBase extends InitializingComponent {
|
||||
|
||||
protected completionPromise = new Deferred<ConnectToControllerDialogModel | undefined>();
|
||||
protected id!: string;
|
||||
protected resources: ResourceInfo[] = [];
|
||||
|
||||
constructor(protected treeDataProvider: AzureArcTreeDataProvider, title: string) {
|
||||
super();
|
||||
@@ -82,6 +83,7 @@ abstract class ControllerDialogBase extends InitializingComponent {
|
||||
|
||||
public showDialog(controllerInfo?: ControllerInfo, password: string | undefined = undefined): azdata.window.Dialog {
|
||||
this.id = controllerInfo?.id ?? uuid();
|
||||
this.resources = controllerInfo?.resources ?? [];
|
||||
this.dialog.cancelButton.onClick(() => this.handleCancel());
|
||||
this.dialog.registerContent(async (view) => {
|
||||
this.modelBuilder = view.modelBuilder;
|
||||
@@ -168,7 +170,7 @@ export class ConnectToControllerDialog extends ControllerDialogBase {
|
||||
name: this.nameInputBox.value ?? '',
|
||||
username: this.usernameInputBox.value,
|
||||
rememberPassword: this.rememberPwCheckBox.checked ?? false,
|
||||
resources: []
|
||||
resources: this.resources
|
||||
};
|
||||
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
|
||||
try {
|
||||
|
||||
135
extensions/arc/src/ui/dialogs/connectSqlDialog.ts
Normal file
135
extensions/arc/src/ui/dialogs/connectSqlDialog.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { Deferred } from '../../common/promise';
|
||||
import { createCredentialId } from '../../common/utils';
|
||||
import { credentialNamespace } from '../../constants';
|
||||
import * as loc from '../../localizedConstants';
|
||||
import { ControllerModel } from '../../models/controllerModel';
|
||||
import { MiaaModel } from '../../models/miaaModel';
|
||||
import { InitializingComponent } from '../components/initializingComponent';
|
||||
|
||||
export class ConnectToSqlDialog extends InitializingComponent {
|
||||
private modelBuilder!: azdata.ModelBuilder;
|
||||
|
||||
private serverNameInputBox!: azdata.InputBoxComponent;
|
||||
private usernameInputBox!: azdata.InputBoxComponent;
|
||||
private passwordInputBox!: azdata.InputBoxComponent;
|
||||
private rememberPwCheckBox!: azdata.CheckBoxComponent;
|
||||
|
||||
private _completionPromise = new Deferred<azdata.IConnectionProfile | undefined>();
|
||||
|
||||
constructor(private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) {
|
||||
super();
|
||||
}
|
||||
|
||||
public showDialog(connectionProfile?: azdata.IConnectionProfile): azdata.window.Dialog {
|
||||
const dialog = azdata.window.createModelViewDialog(loc.connectToSql(this._miaaModel.info.name));
|
||||
dialog.cancelButton.onClick(() => this.handleCancel());
|
||||
dialog.registerContent(async view => {
|
||||
this.modelBuilder = view.modelBuilder;
|
||||
|
||||
this.serverNameInputBox = this.modelBuilder.inputBox()
|
||||
.withProperties<azdata.InputBoxProperties>({
|
||||
value: connectionProfile?.serverName,
|
||||
enabled: false
|
||||
}).component();
|
||||
this.usernameInputBox = this.modelBuilder.inputBox()
|
||||
.withProperties<azdata.InputBoxProperties>({
|
||||
value: connectionProfile?.userName
|
||||
}).component();
|
||||
this.passwordInputBox = this.modelBuilder.inputBox()
|
||||
.withProperties<azdata.InputBoxProperties>({
|
||||
inputType: 'password',
|
||||
value: connectionProfile?.password
|
||||
})
|
||||
.component();
|
||||
this.rememberPwCheckBox = this.modelBuilder.checkBox()
|
||||
.withProperties<azdata.CheckBoxProperties>({
|
||||
label: loc.rememberPassword,
|
||||
checked: connectionProfile?.savePassword
|
||||
}).component();
|
||||
|
||||
let formModel = this.modelBuilder.formContainer()
|
||||
.withFormItems([{
|
||||
components: [
|
||||
{
|
||||
component: this.serverNameInputBox,
|
||||
title: loc.serverEndpoint,
|
||||
required: true
|
||||
}, {
|
||||
component: this.usernameInputBox,
|
||||
title: loc.username,
|
||||
required: true
|
||||
}, {
|
||||
component: this.passwordInputBox,
|
||||
title: loc.password,
|
||||
required: true
|
||||
}, {
|
||||
component: this.rememberPwCheckBox,
|
||||
title: ''
|
||||
}
|
||||
],
|
||||
title: ''
|
||||
}]).withLayout({ width: '100%' }).component();
|
||||
await view.initializeModel(formModel);
|
||||
this.serverNameInputBox.focus();
|
||||
this.initialized = true;
|
||||
});
|
||||
|
||||
dialog.registerCloseValidator(async () => await this.validate());
|
||||
dialog.okButton.label = loc.connect;
|
||||
dialog.cancelButton.label = loc.cancel;
|
||||
azdata.window.openDialog(dialog);
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public async validate(): Promise<boolean> {
|
||||
if (!this.serverNameInputBox.value || !this.usernameInputBox.value || !this.passwordInputBox.value) {
|
||||
return false;
|
||||
}
|
||||
const connectionProfile: azdata.IConnectionProfile = {
|
||||
serverName: this.serverNameInputBox.value,
|
||||
databaseName: '',
|
||||
authenticationType: 'SqlLogin',
|
||||
providerName: 'MSSQL',
|
||||
connectionName: '',
|
||||
userName: this.usernameInputBox.value,
|
||||
password: this.passwordInputBox.value,
|
||||
savePassword: !!this.rememberPwCheckBox.checked,
|
||||
groupFullName: undefined,
|
||||
saveProfile: true,
|
||||
id: '',
|
||||
groupId: undefined,
|
||||
options: {}
|
||||
};
|
||||
const result = await azdata.connection.connect(connectionProfile, false, false);
|
||||
if (result.connected) {
|
||||
connectionProfile.id = result.connectionId;
|
||||
const credentialProvider = await azdata.credentials.getProvider(credentialNamespace);
|
||||
if (connectionProfile.savePassword) {
|
||||
await credentialProvider.saveCredential(createCredentialId(this._controllerModel.info.id, this._miaaModel.info.resourceType, this._miaaModel.info.name), connectionProfile.password);
|
||||
} else {
|
||||
await credentialProvider.deleteCredential(createCredentialId(this._controllerModel.info.id, this._miaaModel.info.resourceType, this._miaaModel.info.name));
|
||||
}
|
||||
this._completionPromise.resolve(connectionProfile);
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
vscode.window.showErrorMessage(loc.connectToSqlFailed(this.serverNameInputBox.value, result.errorMessage));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private handleCancel(): void {
|
||||
this._completionPromise.resolve(undefined);
|
||||
}
|
||||
|
||||
public waitForClose(): Promise<azdata.IConnectionProfile | undefined> {
|
||||
return this._completionPromise.promise;
|
||||
}
|
||||
}
|
||||
@@ -135,10 +135,14 @@ export class AzureArcTreeDataProvider implements vscode.TreeDataProvider<TreeNod
|
||||
if (resourceNode) {
|
||||
await resourceNode.openDashboard();
|
||||
} else {
|
||||
console.log(`Couldn't find resource node for ${name} (${resourceType})`);
|
||||
const errMsg = `Couldn't find resource node for ${name} (${resourceType})`;
|
||||
console.log(errMsg);
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
} else {
|
||||
console.log('Couldn\'t find controller node for opening dashboard');
|
||||
const errMsg = 'Couldn\'t find controller node for opening dashboard';
|
||||
console.log(errMsg);
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,18 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ResourceInfo, ResourceType } from 'arc';
|
||||
import { MiaaResourceInfo, ResourceInfo, ResourceType } from 'arc';
|
||||
import * as vscode from 'vscode';
|
||||
import { parseInstanceName, UserCancelledError } from '../../common/utils';
|
||||
import { UserCancelledError } from '../../common/utils';
|
||||
import * as loc from '../../localizedConstants';
|
||||
import { ControllerModel, Registration } from '../../models/controllerModel';
|
||||
import { MiaaModel } from '../../models/miaaModel';
|
||||
import { PostgresModel } from '../../models/postgresModel';
|
||||
import { ResourceModel } from '../../models/resourceModel';
|
||||
import { ControllerDashboard } from '../dashboards/controller/controllerDashboard';
|
||||
import { AzureArcTreeDataProvider } from './azureArcTreeDataProvider';
|
||||
import { MiaaTreeNode } from './miaaTreeNode';
|
||||
import { NoInstancesTreeNode } from './noInstancesTreeNode';
|
||||
import { PostgresTreeNode } from './postgresTreeNode';
|
||||
import { RefreshTreeNode } from './refreshTreeNode';
|
||||
import { ResourceTreeNode } from './resourceTreeNode';
|
||||
@@ -23,7 +25,7 @@ import { TreeNode } from './treeNode';
|
||||
*/
|
||||
export class ControllerTreeNode extends TreeNode {
|
||||
|
||||
private _children: ResourceTreeNode[] = [];
|
||||
private _children: ResourceTreeNode<ResourceModel>[] = [];
|
||||
|
||||
constructor(public model: ControllerModel, private _context: vscode.ExtensionContext, private _treeDataProvider: AzureArcTreeDataProvider) {
|
||||
super(model.label, vscode.TreeItemCollapsibleState.Collapsed, ResourceType.dataControllers);
|
||||
@@ -55,7 +57,7 @@ export class ControllerTreeNode extends TreeNode {
|
||||
}
|
||||
}
|
||||
|
||||
return this._children;
|
||||
return this._children.length > 0 ? this._children : [new NoInstancesTreeNode()];
|
||||
}
|
||||
|
||||
public async openDashboard(): Promise<void> {
|
||||
@@ -68,14 +70,14 @@ export class ControllerTreeNode extends TreeNode {
|
||||
* @param resourceType The resourceType of the node
|
||||
* @param name The name of the node
|
||||
*/
|
||||
public getResourceNode(resourceType: string, name: string): ResourceTreeNode | undefined {
|
||||
public getResourceNode(resourceType: string, name: string): ResourceTreeNode<ResourceModel> | undefined {
|
||||
return this._children.find(c =>
|
||||
c.model?.info.resourceType === resourceType &&
|
||||
c.model.info.name === name);
|
||||
}
|
||||
|
||||
private updateChildren(registrations: Registration[]): void {
|
||||
const newChildren: ResourceTreeNode[] = [];
|
||||
const newChildren: ResourceTreeNode<ResourceModel>[] = [];
|
||||
registrations.forEach(registration => {
|
||||
if (!registration.instanceName) {
|
||||
console.warn('Registration is missing required name value, skipping');
|
||||
@@ -83,7 +85,7 @@ export class ControllerTreeNode extends TreeNode {
|
||||
}
|
||||
|
||||
const resourceInfo: ResourceInfo = {
|
||||
name: parseInstanceName(registration.instanceName),
|
||||
name: registration.instanceName,
|
||||
resourceType: registration.instanceType ?? ''
|
||||
};
|
||||
|
||||
@@ -100,10 +102,14 @@ export class ControllerTreeNode extends TreeNode {
|
||||
|
||||
switch (registration.instanceType) {
|
||||
case ResourceType.postgresInstances:
|
||||
const postgresModel = new PostgresModel(resourceInfo, registration);
|
||||
const postgresModel = new PostgresModel(this.model, resourceInfo, registration);
|
||||
node = new PostgresTreeNode(postgresModel, this.model, this._context);
|
||||
break;
|
||||
case ResourceType.sqlManagedInstances:
|
||||
// Fill in the username too if we already have it
|
||||
(resourceInfo as MiaaResourceInfo).userName = (this.model.info.resources.find(info =>
|
||||
info.name === resourceInfo.name &&
|
||||
info.resourceType === resourceInfo.resourceType) as MiaaResourceInfo)?.userName;
|
||||
const miaaModel = new MiaaModel(this.model, resourceInfo, registration, this._treeDataProvider);
|
||||
node = new MiaaTreeNode(miaaModel, this.model);
|
||||
break;
|
||||
|
||||
@@ -8,15 +8,15 @@ import * as vscode from 'vscode';
|
||||
import { ControllerModel } from '../../models/controllerModel';
|
||||
import { MiaaModel } from '../../models/miaaModel';
|
||||
import { MiaaDashboard } from '../dashboards/miaa/miaaDashboard';
|
||||
import { TreeNode } from './treeNode';
|
||||
import { ResourceTreeNode } from './resourceTreeNode';
|
||||
|
||||
/**
|
||||
* The TreeNode for displaying a SQL Managed Instance on Azure Arc
|
||||
*/
|
||||
export class MiaaTreeNode extends TreeNode {
|
||||
export class MiaaTreeNode extends ResourceTreeNode<MiaaModel> {
|
||||
|
||||
constructor(public model: MiaaModel, private _controllerModel: ControllerModel) {
|
||||
super(model.info.name, vscode.TreeItemCollapsibleState.None, ResourceType.sqlManagedInstances);
|
||||
constructor(model: MiaaModel, private _controllerModel: ControllerModel) {
|
||||
super(model.info.name, vscode.TreeItemCollapsibleState.None, ResourceType.sqlManagedInstances, model);
|
||||
}
|
||||
|
||||
public async openDashboard(): Promise<void> {
|
||||
|
||||
18
extensions/arc/src/ui/tree/noInstancesTreeNode.ts
Normal file
18
extensions/arc/src/ui/tree/noInstancesTreeNode.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as loc from '../../localizedConstants';
|
||||
import { TreeNode } from './treeNode';
|
||||
|
||||
/**
|
||||
* A placeholder TreeNode to display when there aren't any child instances available
|
||||
*/
|
||||
export class NoInstancesTreeNode extends TreeNode {
|
||||
|
||||
constructor() {
|
||||
super(loc.noInstancesAvailable, vscode.TreeItemCollapsibleState.None, '');
|
||||
}
|
||||
}
|
||||
@@ -13,14 +13,14 @@ import { ResourceTreeNode } from './resourceTreeNode';
|
||||
/**
|
||||
* The TreeNode for displaying an Postgres Server group
|
||||
*/
|
||||
export class PostgresTreeNode extends ResourceTreeNode {
|
||||
export class PostgresTreeNode extends ResourceTreeNode<PostgresModel> {
|
||||
|
||||
constructor(private _model: PostgresModel, private _controllerModel: ControllerModel, private _context: vscode.ExtensionContext) {
|
||||
super(_model.name, vscode.TreeItemCollapsibleState.None, ResourceType.postgresInstances, _model);
|
||||
constructor(model: PostgresModel, private _controllerModel: ControllerModel, private _context: vscode.ExtensionContext) {
|
||||
super(model.info.name, vscode.TreeItemCollapsibleState.None, ResourceType.postgresInstances, model);
|
||||
}
|
||||
|
||||
public async openDashboard(): Promise<void> {
|
||||
const postgresDashboard = new PostgresDashboard(this._context, this._controllerModel, this._model);
|
||||
const postgresDashboard = new PostgresDashboard(this._context, this._controllerModel, this.model);
|
||||
await postgresDashboard.showDashboard();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -10,8 +10,8 @@ import { TreeNode } from './treeNode';
|
||||
/**
|
||||
* A TreeNode belonging to a child of a Controller
|
||||
*/
|
||||
export abstract class ResourceTreeNode extends TreeNode {
|
||||
constructor(label: string, collapsibleState: vscode.TreeItemCollapsibleState, resourceType?: string, public model?: ResourceModel) {
|
||||
export abstract class ResourceTreeNode<M extends ResourceModel> extends TreeNode {
|
||||
constructor(label: string, collapsibleState: vscode.TreeItemCollapsibleState, resourceType: string, public model: M) {
|
||||
super(label, collapsibleState, resourceType);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Welcome to Microsoft Azure Data CLI Extension for Azure Data Studio!
|
||||
|
||||
**This extension is only applicable to customers in the Azure Arc data services private preview. Other usage is not supported at this time.**
|
||||
**This extension is only applicable to customers in the Azure Arc data services public preview. Other usage is not supported at this time.**
|
||||
|
||||
## Overview
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.5 KiB |
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,11 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
|
||||
},
|
||||
postgres: {
|
||||
server: {
|
||||
delete: async (name: string) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.postgres.server.delete(name);
|
||||
},
|
||||
list: async () => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
@@ -81,6 +86,26 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.postgres.server.show(name);
|
||||
},
|
||||
edit: async (
|
||||
name: string,
|
||||
args: {
|
||||
adminPassword?: boolean;
|
||||
coresLimit?: string;
|
||||
coresRequest?: string;
|
||||
engineSettings?: string;
|
||||
extensions?: string;
|
||||
memoryLimit?: string;
|
||||
memoryRequest?: string;
|
||||
noWait?: boolean;
|
||||
port?: number;
|
||||
replaceEngineSettings?: boolean;
|
||||
workers?: number;
|
||||
},
|
||||
additionalEnvVars?: { [key: string]: string; }) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.postgres.server.edit(name, args, additionalEnvVars);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,13 +6,15 @@
|
||||
import * as azdataExt from 'azdata-ext';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { SemVer } from 'semver';
|
||||
import * as vscode from 'vscode';
|
||||
import { getPlatformDownloadLink, getPlatformReleaseVersion } from './azdataReleaseInfo';
|
||||
import { executeCommand, executeSudoCommand, ExitCodeError, ProcessOutput } from './common/childProcess';
|
||||
import { HttpClient } from './common/httpClient';
|
||||
import Logger from './common/logger';
|
||||
import { getErrorMessage, searchForCmd } from './common/utils';
|
||||
import { azdataAcceptEulaKey, azdataConfigSection, azdataFound, azdataHostname, azdataInstallKey, azdataReleaseJson, azdataUpdateKey, azdataUri, debugConfigKey, eulaAccepted, eulaUrl, microsoftPrivacyStatementUrl } from './constants';
|
||||
import { getErrorMessage, NoAzdataError, searchForCmd } from './common/utils';
|
||||
import { azdataAcceptEulaKey, azdataConfigSection, azdataFound, azdataInstallKey, azdataUpdateKey, debugConfigKey, eulaAccepted, eulaUrl, microsoftPrivacyStatementUrl } from './constants';
|
||||
import * as loc from './localizedConstants';
|
||||
|
||||
const enum AzdataDeployOption {
|
||||
@@ -92,11 +94,44 @@ export class AzdataTool implements azdataExt.IAzdataApi {
|
||||
},
|
||||
postgres: {
|
||||
server: {
|
||||
delete: async (name: string) => {
|
||||
return this.executeCommand<void>(['arc', 'postgres', 'server', 'delete', '-n', name]);
|
||||
},
|
||||
list: async () => {
|
||||
return this.executeCommand<azdataExt.PostgresServerListResult[]>(['arc', 'postgres', 'server', 'list']);
|
||||
},
|
||||
show: async (name: string) => {
|
||||
return this.executeCommand<azdataExt.PostgresServerShowResult>(['arc', 'postgres', 'server', 'show', '-n', name]);
|
||||
},
|
||||
edit: async (
|
||||
name: string,
|
||||
args: {
|
||||
adminPassword?: boolean,
|
||||
coresLimit?: string,
|
||||
coresRequest?: string,
|
||||
engineSettings?: string,
|
||||
extensions?: string,
|
||||
memoryLimit?: string,
|
||||
memoryRequest?: string,
|
||||
noWait?: boolean,
|
||||
port?: number,
|
||||
replaceEngineSettings?: boolean,
|
||||
workers?: number
|
||||
},
|
||||
additionalEnvVars?: { [key: string]: string }) => {
|
||||
const argsArray = ['arc', 'postgres', 'server', 'edit', '-n', name];
|
||||
if (args.adminPassword) { argsArray.push('--admin-password'); }
|
||||
if (args.coresLimit !== undefined) { argsArray.push('--cores-limit', args.coresLimit); }
|
||||
if (args.coresRequest !== undefined) { argsArray.push('--cores-request', args.coresRequest); }
|
||||
if (args.engineSettings !== undefined) { argsArray.push('--engine-settings', args.engineSettings); }
|
||||
if (args.extensions !== undefined) { argsArray.push('--extensions', args.extensions); }
|
||||
if (args.memoryLimit !== undefined) { argsArray.push('--memory-limit', args.memoryLimit); }
|
||||
if (args.memoryRequest !== undefined) { argsArray.push('--memory-request', args.memoryRequest); }
|
||||
if (args.noWait) { argsArray.push('--no-wait'); }
|
||||
if (args.port !== undefined) { argsArray.push('--port', args.port.toString()); }
|
||||
if (args.replaceEngineSettings) { argsArray.push('--replace-engine-settings'); }
|
||||
if (args.workers !== undefined) { argsArray.push('--workers', args.workers.toString()); }
|
||||
return this.executeCommand<void>(argsArray, additionalEnvVars);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -151,18 +186,18 @@ export class AzdataTool implements azdataExt.IAzdataApi {
|
||||
// ERROR: { stderr: '...' }
|
||||
// so we also need to trim off the start that isn't a valid JSON blob
|
||||
err.stderr = JSON.parse(err.stderr.substring(err.stderr.indexOf('{'), err.stderr.indexOf('}') + 1)).stderr;
|
||||
} catch (err) {
|
||||
} catch {
|
||||
// it means this was probably some other generic error (such as command not being found)
|
||||
// check if azdata still exists if it does then rethrow the original error if not then emit a new specific error.
|
||||
try {
|
||||
await fs.promises.access(this._path);
|
||||
//this.path exists
|
||||
throw err; // rethrow the error
|
||||
} catch (e) {
|
||||
// this.path does not exist
|
||||
await vscode.commands.executeCommand('setContext', azdataFound, false);
|
||||
throw (loc.noAzdata);
|
||||
throw new NoAzdataError();
|
||||
}
|
||||
throw err; // rethrow the original error
|
||||
}
|
||||
|
||||
}
|
||||
@@ -311,6 +346,7 @@ async function promptToInstallAzdata(userRequested: boolean = false): Promise<bo
|
||||
? [loc.yes, loc.no]
|
||||
: [loc.yes, loc.askLater, loc.doNotAskAgain];
|
||||
if (config === AzdataDeployOption.prompt) {
|
||||
Logger.log(loc.promptForAzdataInstallLog);
|
||||
response = await vscode.window.showErrorMessage(loc.promptForAzdataInstall, ...responses);
|
||||
Logger.log(loc.userResponseToInstallPrompt(response));
|
||||
}
|
||||
@@ -354,6 +390,7 @@ async function promptToUpdateAzdata(newVersion: string, userRequested: boolean =
|
||||
? [loc.yes, loc.no]
|
||||
: [loc.yes, loc.askLater, loc.doNotAskAgain];
|
||||
if (config === AzdataDeployOption.prompt) {
|
||||
Logger.log(loc.promptForAzdataUpdateLog(newVersion));
|
||||
response = await vscode.window.showInformationMessage(loc.promptForAzdataUpdate(newVersion), ...responses);
|
||||
Logger.log(loc.userResponseToUpdatePrompt(response));
|
||||
}
|
||||
@@ -427,9 +464,16 @@ export async function promptForEula(memento: vscode.Memento, userRequested: bool
|
||||
* Downloads the Windows installer and runs it
|
||||
*/
|
||||
async function downloadAndInstallAzdataWin32(): Promise<void> {
|
||||
const downLoadLink = await getPlatformDownloadLink();
|
||||
const downloadFolder = os.tmpdir();
|
||||
const downloadedFile = await HttpClient.downloadFile(`${azdataHostname}/${azdataUri}`, downloadFolder);
|
||||
await executeCommand('msiexec', ['/qn', '/i', downloadedFile]);
|
||||
const downloadLogs = path.join(downloadFolder, 'ads_azdata_install_logs.log');
|
||||
const downloadedFile = await HttpClient.downloadFile(downLoadLink, downloadFolder);
|
||||
|
||||
try {
|
||||
await executeSudoCommand(`msiexec /qn /i "${downloadedFile}" /lvx "${downloadLogs}"`);
|
||||
} catch (err) {
|
||||
throw new Error(`${err.message}. See logs at ${downloadLogs} for more details.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -502,27 +546,10 @@ export async function discoverLatestAvailableAzdataVersion(): Promise<SemVer> {
|
||||
// However, doing discovery that way required apt update to be performed which requires sudo privileges. At least currently this code path
|
||||
// gets invoked on extension start up and prompt user for sudo privileges is annoying at best. So for now basing linux discovery also on a releaseJson file.
|
||||
default:
|
||||
return await discoverLatestAzdataVersionFromJson();
|
||||
return await getPlatformReleaseVersion();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the latest azdata version from a json document published by azdata release
|
||||
*/
|
||||
async function discoverLatestAzdataVersionFromJson(): Promise<SemVer> {
|
||||
// get version information for current platform from http://aka.ms/azdata/release.json
|
||||
const fileContents = await HttpClient.getTextContent(`${azdataHostname}/${azdataReleaseJson}`);
|
||||
let azdataReleaseInfo;
|
||||
try {
|
||||
azdataReleaseInfo = JSON.parse(fileContents);
|
||||
} catch (e) {
|
||||
throw Error(`failed to parse the JSON of contents at: ${azdataHostname}/${azdataReleaseJson}, text being parsed: '${fileContents}', error:${getErrorMessage(e)}`);
|
||||
}
|
||||
const version = azdataReleaseInfo[process.platform]['version'];
|
||||
Logger.log(loc.latestAzdataVersionAvailable(version));
|
||||
return new SemVer(version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses out the azdata version from the raw azdata version output
|
||||
* @param raw The raw version output from azdata --version
|
||||
|
||||
76
extensions/azdata/src/azdataReleaseInfo.ts
Normal file
76
extensions/azdata/src/azdataReleaseInfo.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as os from 'os';
|
||||
import * as loc from './localizedConstants';
|
||||
import { SemVer } from 'semver';
|
||||
import { HttpClient } from './common/httpClient';
|
||||
import Logger from './common/logger';
|
||||
import { getErrorMessage } from './common/utils';
|
||||
import { azdataHostname, azdataReleaseJson } from './constants';
|
||||
|
||||
interface PlatformReleaseInfo {
|
||||
version: string; // "20.0.1"
|
||||
link?: string; // "https://aka.ms/azdata-msi"
|
||||
}
|
||||
|
||||
interface AzdataReleaseInfo {
|
||||
win32: PlatformReleaseInfo,
|
||||
darwin: PlatformReleaseInfo,
|
||||
linux: PlatformReleaseInfo
|
||||
}
|
||||
|
||||
function getPlatformAzdataReleaseInfo(releaseInfo: AzdataReleaseInfo): PlatformReleaseInfo {
|
||||
switch (os.platform()) {
|
||||
case 'win32':
|
||||
return releaseInfo.win32;
|
||||
case 'linux':
|
||||
return releaseInfo.linux;
|
||||
case 'darwin':
|
||||
return releaseInfo.darwin;
|
||||
default:
|
||||
Logger.log(loc.platformUnsupported(os.platform()));
|
||||
throw new Error(`Unsupported AzdataReleaseInfo platform '${os.platform()}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the release version for the current platform from the release info - throwing an error if it doesn't exist.
|
||||
* @param releaseInfo The AzdataReleaseInfo object
|
||||
*/
|
||||
export async function getPlatformReleaseVersion(): Promise<SemVer> {
|
||||
const releaseInfo = await getAzdataReleaseInfo();
|
||||
const platformReleaseInfo = getPlatformAzdataReleaseInfo(releaseInfo);
|
||||
if (!platformReleaseInfo.version) {
|
||||
Logger.log(loc.noReleaseVersion(os.platform(), JSON.stringify(releaseInfo)));
|
||||
throw new Error(`No release version available for platform ${os.platform()}`);
|
||||
}
|
||||
Logger.log(loc.latestAzdataVersionAvailable(platformReleaseInfo.version));
|
||||
return new SemVer(platformReleaseInfo.version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the download link for the current platform from the release info - throwing an error if it doesn't exist.
|
||||
* @param releaseInfo The AzdataReleaseInfo object
|
||||
*/
|
||||
export async function getPlatformDownloadLink(): Promise<string> {
|
||||
const releaseInfo = await getAzdataReleaseInfo();
|
||||
const platformReleaseInfo = getPlatformAzdataReleaseInfo(releaseInfo);
|
||||
if (!platformReleaseInfo.link) {
|
||||
Logger.log(loc.noDownloadLink(os.platform(), JSON.stringify(releaseInfo)));
|
||||
throw new Error(`No download link available for platform ${os.platform()}`);
|
||||
}
|
||||
return platformReleaseInfo.link;
|
||||
}
|
||||
|
||||
async function getAzdataReleaseInfo(): Promise<AzdataReleaseInfo> {
|
||||
const fileContents = await HttpClient.getTextContent(`${azdataHostname}/${azdataReleaseJson}`);
|
||||
try {
|
||||
return JSON.parse(fileContents);
|
||||
} catch (e) {
|
||||
Logger.log(loc.failedToParseReleaseInfo(`${azdataHostname}/${azdataReleaseJson}`, fileContents, e));
|
||||
throw Error(`Failed to parse the JSON of contents at: ${azdataHostname}/${azdataReleaseJson}. Error: ${getErrorMessage(e)}`);
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ export async function executeCommand(command: string, args: string[], additional
|
||||
Logger.log(loc.stdoutOutput(stdout));
|
||||
}
|
||||
if (stderr) {
|
||||
Logger.log(loc.stdoutOutput(stderr));
|
||||
Logger.log(loc.stderrOutput(stderr));
|
||||
}
|
||||
if (code) {
|
||||
const err = new ExitCodeError(code, stderr);
|
||||
@@ -94,7 +94,7 @@ export async function executeSudoCommand(command: string): Promise<ProcessOutput
|
||||
Logger.log(loc.stdoutOutput(stdout));
|
||||
}
|
||||
if (stderr) {
|
||||
Logger.log(loc.stdoutOutput(stderr));
|
||||
Logger.log(loc.stderrOutput(stderr));
|
||||
}
|
||||
if (error) {
|
||||
Logger.log(loc.unexpectedCommandError(error.message));
|
||||
|
||||
@@ -61,7 +61,7 @@ export namespace HttpClient {
|
||||
if (targetFolder !== undefined) {
|
||||
const filename = path.basename(response.request.path);
|
||||
const targetPath = path.join(targetFolder, filename);
|
||||
Logger.log(loc.downloadingTo(filename, targetPath));
|
||||
Logger.log(loc.downloadingTo(filename, downloadUrl, targetPath));
|
||||
// Wait to create the WriteStream until here so we can use the actual
|
||||
// filename based off of the URI.
|
||||
downloadRequest.pipe(fs.createWriteStream(targetPath))
|
||||
@@ -73,6 +73,11 @@ export namespace HttpClient {
|
||||
reject(downloadError);
|
||||
downloadRequest.abort();
|
||||
});
|
||||
} else {
|
||||
response.on('end', () => {
|
||||
Logger.log(loc.downloadFinished);
|
||||
resolve(strings.join(''));
|
||||
});
|
||||
}
|
||||
let contentLength = response.headers['content-length'];
|
||||
let totalBytes = parseInt(contentLength || '0');
|
||||
@@ -92,13 +97,6 @@ export namespace HttpClient {
|
||||
printThreshold += 0.1;
|
||||
}
|
||||
}
|
||||
})
|
||||
.on('close', async () => {
|
||||
if (targetFolder === undefined) {
|
||||
|
||||
Logger.log(loc.downloadFinished);
|
||||
resolve(strings.join(''));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -34,15 +34,6 @@ export async function activate(context: vscode.ExtensionContext): Promise<azdata
|
||||
eulaAccepted = isEulaAccepted(context.globalState); // fetch eula acceptance state from memento
|
||||
await vscode.commands.executeCommand('setContext', constants.eulaAccepted, eulaAccepted); // set a context key for current value of eulaAccepted state retrieved from memento so that command for accepting eula is available/unavailable in commandPalette appropriately.
|
||||
Logger.log(loc.eulaAcceptedStateOnStartup(eulaAccepted));
|
||||
if (!eulaAccepted) {
|
||||
// Don't block on this since we want extension to finish activating without requiring user actions.
|
||||
// If EULA has not been accepted then we will check again while executing azdata commands.
|
||||
promptForEula(context.globalState)
|
||||
.then(async (userResponse: boolean) => {
|
||||
eulaAccepted = userResponse;
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
}
|
||||
|
||||
// Don't block on this since we want the extension to finish activating without needing user input
|
||||
const localAzdataDiscovered = checkAndInstallAzdata() // install if not installed and user wants it.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as should from 'should';
|
||||
import * as sinon from 'sinon';
|
||||
import * as vscode from 'vscode';
|
||||
@@ -11,10 +10,14 @@ import * as azdata from '../azdata';
|
||||
import * as childProcess from '../common/childProcess';
|
||||
import { HttpClient } from '../common/httpClient';
|
||||
import * as utils from '../common/utils';
|
||||
import * as constants from '../constants';
|
||||
import * as loc from '../localizedConstants';
|
||||
|
||||
const oldAzdataMock = new azdata.AzdataTool('/path/to/azdata', '0.0.0');
|
||||
|
||||
/**
|
||||
* This matches the schema of the JSON file used to determine the current version of
|
||||
* azdata - do not modify unless also updating the corresponding JSON file
|
||||
*/
|
||||
const releaseJson = {
|
||||
win32: {
|
||||
'version': '9999.999.999',
|
||||
@@ -27,7 +30,7 @@ const releaseJson = {
|
||||
'version': '9999.999.999'
|
||||
}
|
||||
};
|
||||
|
||||
let executeSudoCommandStub: sinon.SinonStub;
|
||||
|
||||
describe('azdata', function () {
|
||||
afterEach(function (): void {
|
||||
@@ -55,9 +58,11 @@ describe('azdata', function () {
|
||||
});
|
||||
|
||||
describe('installAzdata', function (): void {
|
||||
|
||||
beforeEach(function (): void {
|
||||
sinon.stub(vscode.window, 'showErrorMessage').returns(Promise.resolve(<any>loc.yes));
|
||||
sinon.stub(utils, 'searchForCmd').returns(Promise.resolve('/path/to/azdata'));
|
||||
executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand').returns(Promise.resolve({ stdout: '', stderr: '' }));
|
||||
});
|
||||
|
||||
it('successful install', async function (): Promise<void> {
|
||||
@@ -77,8 +82,12 @@ describe('azdata', function () {
|
||||
if (process.platform === 'win32') {
|
||||
it('unsuccessful download - win32', async function (): Promise<void> {
|
||||
sinon.stub(HttpClient, 'downloadFile').rejects();
|
||||
const downloadPromise = azdata.checkAndInstallAzdata();
|
||||
await should(downloadPromise).be.rejected();
|
||||
sinon.stub(childProcess, 'executeCommand')
|
||||
.onFirstCall()
|
||||
.rejects(new Error('not Found')) // First call mock the tool not being found
|
||||
.resolves({ stdout: '1.0.0', stderr: '' });
|
||||
const azdataTool = await azdata.checkAndInstallAzdata();
|
||||
should(azdataTool).be.undefined();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -100,6 +109,7 @@ describe('azdata', function () {
|
||||
describe('updateAzdata', function (): void {
|
||||
beforeEach(function (): void {
|
||||
sinon.stub(vscode.window, 'showInformationMessage').returns(Promise.resolve(<any>loc.yes));
|
||||
executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand').returns(Promise.resolve({ stdout: '', stderr: '' }));
|
||||
});
|
||||
|
||||
it('successful update', async function (): Promise<void> {
|
||||
@@ -107,7 +117,6 @@ describe('azdata', function () {
|
||||
case 'win32':
|
||||
await testWin32SuccessfulUpdate();
|
||||
break;
|
||||
|
||||
case 'darwin':
|
||||
await testDarwinSuccessfulUpdate();
|
||||
break;
|
||||
@@ -132,9 +141,8 @@ describe('azdata', function () {
|
||||
});
|
||||
|
||||
describe('discoverLatestAvailableAzdataVersion', function (): void {
|
||||
this.timeout(20000);
|
||||
it(`finds latest available version of azdata successfully`, async function (): Promise<void> {
|
||||
// if the latest version is not discovered then the following call throws failing the test
|
||||
it('finds latest available version of azdata successfully', async function (): Promise<void> {
|
||||
sinon.stub(HttpClient, 'getTextContent').resolves(JSON.stringify(releaseJson));
|
||||
await azdata.discoverLatestAvailableAzdataVersion();
|
||||
});
|
||||
});
|
||||
@@ -142,7 +150,7 @@ describe('azdata', function () {
|
||||
});
|
||||
|
||||
async function testLinuxUnsuccessfulUpdate() {
|
||||
const executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand').rejects();
|
||||
executeSudoCommandStub.rejects();
|
||||
const updateDone = await azdata.checkAndUpdateAzdata(oldAzdataMock);
|
||||
should(updateDone).be.false();
|
||||
should(executeSudoCommandStub.calledOnce).be.true();
|
||||
@@ -172,7 +180,7 @@ async function testDarwinUnsuccessfulUpdate() {
|
||||
return Promise.reject(new Error('not Found'));
|
||||
})
|
||||
.callsFake(async (_command: string, _args: string[]) => { // by default return success
|
||||
return Promise.resolve({stderr: '', stdout: 'success'});
|
||||
return Promise.resolve({ stderr: '', stdout: 'success' });
|
||||
});
|
||||
const updateDone = await azdata.checkAndUpdateAzdata(oldAzdataMock);
|
||||
should(updateDone).be.false();
|
||||
@@ -181,16 +189,16 @@ async function testDarwinUnsuccessfulUpdate() {
|
||||
|
||||
async function testWin32UnsuccessfulUpdate() {
|
||||
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
|
||||
const executeCommandStub = sinon.stub(childProcess, 'executeCommand').rejects();
|
||||
executeSudoCommandStub.rejects();
|
||||
const updateDone = await azdata.checkAndUpdateAzdata(oldAzdataMock);
|
||||
should(updateDone).be.false();
|
||||
should(executeCommandStub.calledOnce).be.true();
|
||||
should(updateDone).be.false('Update should not have been successful');
|
||||
should(executeSudoCommandStub.calledOnce).be.true();
|
||||
}
|
||||
|
||||
async function testLinuxSuccessfulUpdate() {
|
||||
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
|
||||
const executeCommandStub = sinon.stub(childProcess, 'executeCommand').returns(Promise.resolve({ stdout: '0.0.0', stderr: '' }));
|
||||
const executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand').returns(Promise.resolve({ stdout: '0.0.0', stderr: '' }));
|
||||
executeSudoCommandStub.resolves({ stdout: '0.0.0', stderr: '' });
|
||||
await azdata.checkAndUpdateAzdata(oldAzdataMock);
|
||||
should(executeSudoCommandStub.callCount).be.equal(6);
|
||||
should(executeCommandStub.calledOnce).be.true();
|
||||
@@ -209,51 +217,39 @@ async function testDarwinSuccessfulUpdate() {
|
||||
}];
|
||||
const executeCommandStub = sinon.stub(childProcess, 'executeCommand')
|
||||
.onThirdCall() //third call is brew info azdata-cli --json which needs to return json of new available azdata versions.
|
||||
.callsFake(async (command: string, args: string[]) => {
|
||||
should(command).be.equal('brew');
|
||||
should(args).deepEqual(['info', 'azdata-cli', '--json']);
|
||||
return Promise.resolve({
|
||||
.resolves({
|
||||
stderr: '',
|
||||
stdout: JSON.stringify(brewInfoOutput)
|
||||
});
|
||||
})
|
||||
.callsFake(async (_command: string, _args: string[]) => { // return success on all other command executions
|
||||
return Promise.resolve({ stdout: '0.0.0', stderr: '' });
|
||||
});
|
||||
.resolves({ stdout: '0.0.0', stderr: '' });
|
||||
await azdata.checkAndUpdateAzdata(oldAzdataMock);
|
||||
should(executeCommandStub.callCount).be.equal(6);
|
||||
should(executeCommandStub.getCall(2).args[0]).be.equal('brew', '3rd call should have been to brew');
|
||||
should(executeCommandStub.getCall(2).args[1]).deepEqual(['info', 'azdata-cli', '--json'], '3rd call did not have expected arguments');
|
||||
}
|
||||
|
||||
|
||||
async function testWin32SuccessfulUpdate() {
|
||||
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
|
||||
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
|
||||
const executeCommandStub = sinon.stub(childProcess, 'executeCommand').callsFake(async (command: string, args: string[]) => {
|
||||
should(command).be.equal('msiexec');
|
||||
should(args[0]).be.equal('/qn');
|
||||
should(args[1]).be.equal('/i');
|
||||
should(path.basename(args[2])).be.equal(constants.azdataUri);
|
||||
return { stdout: '0.0.0', stderr: '' };
|
||||
});
|
||||
await azdata.checkAndUpdateAzdata(oldAzdataMock);
|
||||
should(executeCommandStub.calledOnce).be.true();
|
||||
should(executeSudoCommandStub.calledOnce).be.true('executeSudoCommand should have been called once');
|
||||
should(executeSudoCommandStub.getCall(0).args[0]).startWith('msiexec /qn /i');
|
||||
}
|
||||
|
||||
async function testWin32SuccessfulInstall() {
|
||||
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
|
||||
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
|
||||
const executeCommandStub = sinon.stub(childProcess, 'executeCommand')
|
||||
.onFirstCall()
|
||||
.callsFake(async (_command: string, _args: string[]) => {
|
||||
return Promise.reject(new Error('not Found'));
|
||||
})
|
||||
.callsFake(async (command: string, args: string[]) => {
|
||||
should(command).be.equal('msiexec');
|
||||
should(args[0]).be.equal('/qn');
|
||||
should(args[1]).be.equal('/i');
|
||||
should(path.basename(args[2])).be.equal(constants.azdataUri);
|
||||
return { stdout: '0.0.0', stderr: '' };
|
||||
});
|
||||
.rejects(new Error('not Found')) // First call mock the tool not being found
|
||||
.resolves({ stdout: '1.0.0', stderr: '' });
|
||||
executeSudoCommandStub
|
||||
.returns({ stdout: '', stderr: '' });
|
||||
await azdata.checkAndInstallAzdata();
|
||||
should(executeCommandStub.calledTwice).be.true();
|
||||
should(executeCommandStub.calledTwice).be.true(`executeCommand should have been called twice. Actual ${executeCommandStub.getCalls().length}`);
|
||||
should(executeSudoCommandStub.calledOnce).be.true(`executeSudoCommand should have been called once. Actual ${executeSudoCommandStub.getCalls().length}`);
|
||||
should(executeSudoCommandStub.getCall(0).args[0]).startWith('msiexec /qn /i');
|
||||
}
|
||||
|
||||
async function testDarwinSuccessfulInstall() {
|
||||
@@ -272,23 +268,17 @@ async function testDarwinSuccessfulInstall() {
|
||||
async function testLinuxSuccessfulInstall() {
|
||||
const executeCommandStub = sinon.stub(childProcess, 'executeCommand')
|
||||
.onFirstCall()
|
||||
.callsFake(async (_command: string, _args: string[]) => {
|
||||
return Promise.reject(new Error('not Found'));
|
||||
})
|
||||
.callsFake(async (_command: string, _args: string[]) => {
|
||||
return Promise.resolve({ stdout: '0.0.0', stderr: '' });
|
||||
});
|
||||
const executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand')
|
||||
.callsFake(async (_command: string ) => {
|
||||
return Promise.resolve({ stdout: 'success', stderr: '' });
|
||||
});
|
||||
.rejects(new Error('not Found'))
|
||||
.resolves({ stdout: '0.0.0', stderr: '' });
|
||||
executeSudoCommandStub
|
||||
.resolves({ stdout: 'success', stderr: '' });
|
||||
await azdata.checkAndInstallAzdata();
|
||||
should(executeSudoCommandStub.callCount).be.equal(6);
|
||||
should(executeCommandStub.calledThrice).be.true();
|
||||
}
|
||||
|
||||
async function testLinuxUnsuccessfulInstall() {
|
||||
const executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand').rejects();
|
||||
executeSudoCommandStub.rejects();
|
||||
const downloadPromise = azdata.installAzdata();
|
||||
await should(downloadPromise).be.rejected();
|
||||
should(executeSudoCommandStub.calledOnce).be.true();
|
||||
@@ -302,9 +292,9 @@ async function testDarwinUnsuccessfulInstall() {
|
||||
}
|
||||
|
||||
async function testWin32UnsuccessfulInstall() {
|
||||
const executeCommandStub = sinon.stub(childProcess, 'executeCommand').rejects();
|
||||
executeSudoCommandStub.rejects();
|
||||
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
|
||||
const downloadPromise = azdata.installAzdata();
|
||||
await should(downloadPromise).be.rejected();
|
||||
should(executeCommandStub.calledOnce).be.true();
|
||||
should(executeSudoCommandStub.calledOnce).be.true();
|
||||
}
|
||||
|
||||
@@ -73,12 +73,12 @@ describe('HttpClient', function (): void {
|
||||
});
|
||||
|
||||
describe('getTextContent', function (): void {
|
||||
it.skip('Gets file contents correctly', async function (): Promise<void> {
|
||||
it('Gets file contents correctly', async function (): Promise<void> {
|
||||
nock('https://127.0.0.1')
|
||||
.get('/arbitraryFile')
|
||||
.replyWithFile(200, __filename);
|
||||
const receivedContents = await HttpClient.getTextContent(`https://127.0.0.1/arbitraryFile`);
|
||||
should(receivedContents).equal(await fs.promises.readFile(__filename));
|
||||
should(receivedContents).equal((await fs.promises.readFile(__filename)).toString());
|
||||
});
|
||||
|
||||
it('rejects on response error', async function (): Promise<void> {
|
||||
|
||||
92
extensions/azdata/src/typings/azdata-ext.d.ts
vendored
92
extensions/azdata/src/typings/azdata-ext.d.ts
vendored
@@ -143,25 +143,13 @@ declare module 'azdata-ext' {
|
||||
},
|
||||
status: {
|
||||
readyReplicas: string, // "1/1"
|
||||
state: string, // "Ready"
|
||||
state: string, // "Ready",
|
||||
logSearchDashboard: string, // https://127.0.0.1:30777/kibana/app/kibana#/discover?_a=(query:(language:kuery,query:'custom_resource_name:miaa1'))
|
||||
metricsDashboard: string, // https://127.0.0.1:30777/grafana/d/40q72HnGk/sql-managed-instance-metrics?var-hostname=miaa1-0
|
||||
externalEndpoint?: string // "10.91.86.39:32718"
|
||||
}
|
||||
}
|
||||
|
||||
export interface PostgresServerShowResult {
|
||||
apiVersion: string, // "arcdata.microsoft.com/v1alpha1"
|
||||
kind: string, // "postgresql-12"
|
||||
metadata: {
|
||||
creationTimestamp: string, // "2020-08-19T20:25:11Z"
|
||||
generation: number, // 1
|
||||
name: string, // "chgagnon-pg"
|
||||
namespace: string, // "arc",
|
||||
resourceVersion: string, // "214944",
|
||||
selfLink: string, // "/apis/arcdata.microsoft.com/v1alpha1/namespaces/arc/postgresql-12s/chgagnon-pg",
|
||||
uid: string, // "26d0f5bb-0c0b-4225-a6b5-5be2bf6feac0"
|
||||
}
|
||||
}
|
||||
|
||||
export interface PostgresServerShowResult {
|
||||
apiVersion: string, // "arcdata.microsoft.com/v1alpha1"
|
||||
kind: string, // "postgresql-12"
|
||||
@@ -175,25 +163,56 @@ declare module 'azdata-ext' {
|
||||
uid: string, // "26d0f5bb-0c0b-4225-a6b5-5be2bf6feac0"
|
||||
},
|
||||
spec: {
|
||||
backups: {
|
||||
deltaMinutes: number, // 3,
|
||||
fullMinutes: number, // 10,
|
||||
tiers: [
|
||||
{
|
||||
retention: {
|
||||
maximums: string[], // [ "6", "512MB" ],
|
||||
minimums: string[], // [ "3" ]
|
||||
engine: {
|
||||
extensions: {
|
||||
name: string // "citus"
|
||||
}[],
|
||||
settings: {
|
||||
default: { [key: string]: string } // { "max_connections": "101", "work_mem": "4MB" }
|
||||
}
|
||||
},
|
||||
scale: {
|
||||
shards: number // 1
|
||||
},
|
||||
scheduling: {
|
||||
default: {
|
||||
resources: {
|
||||
requests: {
|
||||
cpu: string, // "1.5"
|
||||
memory: string // "256Mi"
|
||||
},
|
||||
limits: {
|
||||
cpu: string, // "1.5"
|
||||
memory: string // "256Mi"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
service: {
|
||||
type: string, // "NodePort"
|
||||
port: number // 5432
|
||||
},
|
||||
storage: {
|
||||
volumeSize: string, // "1Gi"
|
||||
data: {
|
||||
className: string, // "local-storage"
|
||||
size: string // "5Gi"
|
||||
},
|
||||
logs: {
|
||||
className: string, // "local-storage"
|
||||
size: string // "5Gi"
|
||||
},
|
||||
backups: {
|
||||
className: string, // "local-storage"
|
||||
size: string // "5Gi"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
status: {
|
||||
externalEndpoint: string, // "10.130.12.136:26630"
|
||||
readyPods: string, // "1/1",
|
||||
state: string // "Ready"
|
||||
}
|
||||
state: string, // "Ready"
|
||||
logSearchDashboard: string, // https://127.0.0.1:30777/kibana/app/kibana#/discover?_a=(query:(language:kuery,query:'custom_resource_name:pg1'))
|
||||
metricsDashboard: string, // https://127.0.0.1:30777/grafana/d/40q72HnGk/sql-managed-instance-metrics?var-hostname=pg1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,8 +238,25 @@ declare module 'azdata-ext' {
|
||||
},
|
||||
postgres: {
|
||||
server: {
|
||||
delete(name: string): Promise<AzdataOutput<void>>,
|
||||
list(): Promise<AzdataOutput<PostgresServerListResult[]>>,
|
||||
show(name: string): Promise<AzdataOutput<PostgresServerShowResult>>
|
||||
show(name: string): Promise<AzdataOutput<PostgresServerShowResult>>,
|
||||
edit(
|
||||
name: string,
|
||||
args: {
|
||||
adminPassword?: boolean,
|
||||
coresLimit?: string,
|
||||
coresRequest?: string,
|
||||
engineSettings?: string,
|
||||
extensions?: string,
|
||||
memoryLimit?: string,
|
||||
memoryRequest?: string,
|
||||
noWait?: boolean,
|
||||
port?: number,
|
||||
replaceEngineSettings?: boolean,
|
||||
workers?: number
|
||||
},
|
||||
additionalEnvVars?: { [key: string]: string }): Promise<AzdataOutput<void>>
|
||||
}
|
||||
},
|
||||
sql: {
|
||||
|
||||
Reference in New Issue
Block a user