mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-20 18:46:56 -05:00
Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d296b6397e | ||
|
|
05615c796d | ||
|
|
e60b01ac00 | ||
|
|
b68cdbeebe | ||
|
|
7429407029 | ||
|
|
6adeffbc8e | ||
|
|
8a078d2d68 | ||
|
|
eadac3af3a | ||
|
|
8e8d9b5f59 | ||
|
|
93e806cca1 | ||
|
|
98ed0d5274 | ||
|
|
7bca43524e | ||
|
|
a8c983519e | ||
|
|
ac6ef2639f | ||
|
|
35957cc283 | ||
|
|
b054295eac | ||
|
|
5b7a7c9865 | ||
|
|
867faae14f | ||
|
|
4c6b606c82 | ||
|
|
d5daaf918d | ||
|
|
72d48bda61 | ||
|
|
93156ccf04 | ||
|
|
781c7de772 | ||
|
|
41e8b73ac4 | ||
|
|
61254c7298 | ||
|
|
5f59fa021c | ||
|
|
1f65216889 | ||
|
|
c801d46814 | ||
|
|
6c85cf2bdd | ||
|
|
9067204979 | ||
|
|
ac6bc56c4e | ||
|
|
1b5c54dd8c | ||
|
|
4082170522 | ||
|
|
5ecf1c6e6f | ||
|
|
6de11c8107 | ||
|
|
76d7b0a9fe | ||
|
|
ce4c3e9586 | ||
|
|
5190bf376c | ||
|
|
77b9a708df | ||
|
|
a4ee871b88 | ||
|
|
3f4e19fc08 | ||
|
|
571fca6de5 | ||
|
|
5a2fdc4034 | ||
|
|
cc6d84e7f6 | ||
|
|
99e11d2e22 | ||
|
|
9a85123e21 | ||
|
|
56669db6b6 | ||
|
|
8782eeb32f | ||
|
|
7f3d5bac0a | ||
|
|
7a1e0a7d2e | ||
|
|
681ecbd946 | ||
|
|
e7798a8e32 | ||
|
|
b158180ef4 | ||
|
|
7ad9da7fda | ||
|
|
94e2016a16 | ||
|
|
21bb577da8 | ||
|
|
5e8325ba28 | ||
|
|
25b7ccade3 | ||
|
|
57940c581c | ||
|
|
82f9e4e24b | ||
|
|
3e22fcfd2d | ||
|
|
0bc81e1078 | ||
|
|
7b6328dccf | ||
|
|
05124273ea | ||
|
|
b1d4444522 | ||
|
|
4ee2d369cf | ||
|
|
fb28b69bb0 | ||
|
|
f2709c7100 | ||
|
|
3476f5ae38 | ||
|
|
b937fdee7a | ||
|
|
dd9ac2e362 | ||
|
|
403ff6cfec | ||
|
|
4a6226974e | ||
|
|
6a2c47f511 | ||
|
|
3d9a316f4b |
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Welcome to Microsoft Azure Arc Extension for Azure Data Studio!
|
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
|
## Overview
|
||||||
|
|
||||||
|
|||||||
@@ -8,5 +8,5 @@
|
|||||||
not_numbered: true
|
not_numbered: true
|
||||||
expand_sections: true
|
expand_sections: true
|
||||||
sections:
|
sections:
|
||||||
- title: TSG100 - The Azure Arc Postgres troubleshooter
|
- title: TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter
|
||||||
url: postgres/tsg100-troubleshoot-postgres
|
url: postgres/tsg100-troubleshoot-postgres
|
||||||
|
|||||||
@@ -3,5 +3,5 @@
|
|||||||
- This chapter contains notebooks for troubleshooting Postgres on Azure Arc
|
- This chapter contains notebooks for troubleshooting Postgres on Azure Arc
|
||||||
|
|
||||||
## Notebooks in this Chapter
|
## 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
|
not_numbered: true
|
||||||
expand_sections: true
|
expand_sections: true
|
||||||
sections:
|
sections:
|
||||||
- title: TSG100 - The Azure Arc Postgres troubleshooter
|
- title: TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter
|
||||||
url: postgres/tsg100-troubleshoot-postgres
|
url: postgres/tsg100-troubleshoot-postgres
|
||||||
|
|||||||
@@ -4,13 +4,14 @@
|
|||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"source": [
|
"source": [
|
||||||
"TSG100 - The Azure Arc Postgres troubleshooter\n",
|
"TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter\n",
|
||||||
"==============================================\n",
|
"===================================================================\n",
|
||||||
"\n",
|
"\n",
|
||||||
"Description\n",
|
"Description\n",
|
||||||
"-----------\n",
|
"-----------\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",
|
"\n",
|
||||||
"Steps\n",
|
"Steps\n",
|
||||||
"-----\n",
|
"-----\n",
|
||||||
@@ -34,6 +35,7 @@
|
|||||||
"# the user will be prompted to select a server.\n",
|
"# the user will be prompted to select a server.\n",
|
||||||
"namespace = os.environ.get('POSTGRES_SERVER_NAMESPACE')\n",
|
"namespace = os.environ.get('POSTGRES_SERVER_NAMESPACE')\n",
|
||||||
"name = os.environ.get('POSTGRES_SERVER_NAME')\n",
|
"name = os.environ.get('POSTGRES_SERVER_NAME')\n",
|
||||||
|
"version = os.environ.get('POSTGRES_SERVER_VERSION')\n",
|
||||||
"\n",
|
"\n",
|
||||||
"tail_lines = 50"
|
"tail_lines = 50"
|
||||||
]
|
]
|
||||||
@@ -143,7 +145,7 @@
|
|||||||
" if cmd.startswith(\"kubectl \") and \"AZDATA_OPENSHIFT\" in os.environ:\n",
|
" if cmd.startswith(\"kubectl \") and \"AZDATA_OPENSHIFT\" in os.environ:\n",
|
||||||
" cmd_actual[0] = cmd_actual[0].replace(\"kubectl\", \"oc\")\n",
|
" cmd_actual[0] = cmd_actual[0].replace(\"kubectl\", \"oc\")\n",
|
||||||
"\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",
|
" #\n",
|
||||||
" which_binary = None\n",
|
" which_binary = None\n",
|
||||||
"\n",
|
"\n",
|
||||||
@@ -400,11 +402,11 @@
|
|||||||
"import math\n",
|
"import math\n",
|
||||||
"\n",
|
"\n",
|
||||||
"# If a server was provided, get it\n",
|
"# If a server was provided, get it\n",
|
||||||
"if namespace and name:\n",
|
"if namespace and name and version:\n",
|
||||||
" server = json.loads(run(f'kubectl get dbs -n {namespace} {name} -o json', return_output=True))\n",
|
" server = json.loads(run(f'kubectl get postgresql-{version} -n {namespace} {name} -o json', return_output=True))\n",
|
||||||
"else:\n",
|
"else:\n",
|
||||||
" # Otherwise prompt the user to select a server\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",
|
" if not servers:\n",
|
||||||
" raise Exception('No Postgres servers found')\n",
|
" raise Exception('No Postgres servers found')\n",
|
||||||
"\n",
|
"\n",
|
||||||
@@ -425,6 +427,7 @@
|
|||||||
" server = servers[i-1]\n",
|
" server = servers[i-1]\n",
|
||||||
" namespace = server['metadata']['namespace']\n",
|
" namespace = server['metadata']['namespace']\n",
|
||||||
" name = server['metadata']['name']\n",
|
" name = server['metadata']['name']\n",
|
||||||
|
" version = server['kind'][len('postgresql-'):]\n",
|
||||||
" break\n",
|
" break\n",
|
||||||
"\n",
|
"\n",
|
||||||
"display(Markdown(f'#### Got server {namespace}.{name}'))"
|
"display(Markdown(f'#### Got server {namespace}.{name}'))"
|
||||||
@@ -446,10 +449,10 @@
|
|||||||
"uid = server['metadata']['uid']\n",
|
"uid = server['metadata']['uid']\n",
|
||||||
"\n",
|
"\n",
|
||||||
"display(Markdown(f'#### Server summary'))\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",
|
"\n",
|
||||||
"display(Markdown(f'#### Resource summary'))\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": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"display(Markdown(f'#### Troubleshooting server {namespace}.{name}'))\n",
|
"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": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"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",
|
"\n",
|
||||||
"# Summarize and describe each pod\n",
|
"# Summarize and describe each pod\n",
|
||||||
"for pod in pods:\n",
|
"for pod in pods:\n",
|
||||||
@@ -529,8 +532,7 @@
|
|||||||
" con_restarts = con_status.get('restartCount', 0)\n",
|
" con_restarts = con_status.get('restartCount', 0)\n",
|
||||||
"\n",
|
"\n",
|
||||||
" display(Markdown(f'#### Troubleshooting container {namespace}.{pod_name}/{con_name} ({i+1}/{len(cons)})\\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'#### {\"R\" if con_ready else \"Not r\"}eady with {con_restarts} restarts'))\n",
|
||||||
" f'{\"\" if con_ready else \"not \"}ready with {con_restarts} restarts'))\n",
|
|
||||||
"\n",
|
"\n",
|
||||||
" run(f'kubectl logs -n {namespace} {pod_name} {con_name} --tail {tail_lines}')\n",
|
" run(f'kubectl logs -n {namespace} {pod_name} {con_name} --tail {tail_lines}')\n",
|
||||||
"\n",
|
"\n",
|
||||||
@@ -554,7 +556,7 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"display(Markdown(f'#### Troubleshooting PersistentVolumeClaims'))\n",
|
"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",
|
"|Tools|Description|Installation|\n",
|
||||||
"|---|---|---|\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",
|
"|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": {
|
"metadata": {
|
||||||
"azdata_cell_guid": "714582b9-10ee-409e-ab12-15a4825c9471"
|
"azdata_cell_guid": "714582b9-10ee-409e-ab12-15a4825c9471"
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"source": [
|
"source": [
|
||||||
"### **Set variables**\n",
|
"### **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": {
|
"metadata": {
|
||||||
"azdata_cell_guid": "4b266b2d-bd1b-4565-92c9-3fc146cdce6d"
|
"azdata_cell_guid": "4b266b2d-bd1b-4565-92c9-3fc146cdce6d"
|
||||||
@@ -129,13 +129,11 @@
|
|||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"source": [
|
"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",
|
"if \"AZDATA_NB_VAR_ARC_ADMIN_PASSWORD\" in os.environ:\n",
|
||||||
" arc_admin_password = os.environ[\"AZDATA_NB_VAR_ARC_ADMIN_PASSWORD\"]\n",
|
" arc_admin_password = os.environ[\"AZDATA_NB_VAR_ARC_ADMIN_PASSWORD\"]\n",
|
||||||
"else:\n",
|
"else:\n",
|
||||||
" if arc_admin_password == \"\":\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",
|
" if arc_admin_password == \"\":\n",
|
||||||
" sys.exit(f'Password is required.')\n",
|
" sys.exit(f'Password is required.')\n",
|
||||||
" confirm_password = getpass.getpass(prompt = 'Confirm password')\n",
|
" confirm_password = getpass.getpass(prompt = 'Confirm password')\n",
|
||||||
@@ -175,7 +173,7 @@
|
|||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"source": [
|
"source": [
|
||||||
"### **Create Azure Arc Data controller**"
|
"### **Create Azure Arc Data Controller**"
|
||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"azdata_cell_guid": "efe78cd3-ed73-4c9b-b586-fdd6c07dd37f"
|
"azdata_cell_guid": "efe78cd3-ed73-4c9b-b586-fdd6c07dd37f"
|
||||||
@@ -184,16 +182,14 @@
|
|||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"source": [
|
"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[\"ACCEPT_EULA\"] = 'yes'\n",
|
||||||
"os.environ[\"AZDATA_USERNAME\"] = arc_admin_username\n",
|
"os.environ[\"AZDATA_USERNAME\"] = arc_admin_username\n",
|
||||||
"os.environ[\"AZDATA_PASSWORD\"] = arc_admin_password\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",
|
"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",
|
" 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",
|
"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 cluster: {arc_data_controller_name} created.') "
|
"print(f'Azure Arc Data Controller: {arc_data_controller_name} created.') "
|
||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"azdata_cell_guid": "373947a1-90b9-49ee-86f4-17a4c7d4ca76",
|
"azdata_cell_guid": "373947a1-90b9-49ee-86f4-17a4c7d4ca76",
|
||||||
@@ -205,7 +201,7 @@
|
|||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"source": [
|
"source": [
|
||||||
"### **Setting context to created Azure Arc Data controller**"
|
"### **Setting context to created Azure Arc Data Controller**"
|
||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"azdata_cell_guid": "a3ddc701-811d-4058-b3fb-b7295fcf50ae"
|
"azdata_cell_guid": "a3ddc701-811d-4058-b3fb-b7295fcf50ae"
|
||||||
@@ -214,7 +210,7 @@
|
|||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"source": [
|
"source": [
|
||||||
"# Setting context to data controller.\n",
|
"# Setting context to Data Controller.\n",
|
||||||
"#\n",
|
"#\n",
|
||||||
"run_command(f'kubectl config set-context --current --namespace {arc_data_controller_namespace}')"
|
"run_command(f'kubectl config set-context --current --namespace {arc_data_controller_namespace}')"
|
||||||
],
|
],
|
||||||
@@ -227,7 +223,7 @@
|
|||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"source": [
|
"source": [
|
||||||
"### **Login to the data controller.**\n"
|
"### **Login to the Data Controller.**\n"
|
||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"azdata_cell_guid": "9376b2ab-0edf-478f-9e3c-5ff46ae3501a"
|
"azdata_cell_guid": "9376b2ab-0edf-478f-9e3c-5ff46ae3501a"
|
||||||
@@ -236,9 +232,9 @@
|
|||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"source": [
|
"source": [
|
||||||
"# Login to the data controller.\n",
|
"# Login to the Data Controller.\n",
|
||||||
"#\n",
|
"#\n",
|
||||||
"run_command(f'azdata login -n {arc_data_controller_namespace}')"
|
"run_command(f'azdata login --namespace {arc_data_controller_namespace}')"
|
||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"azdata_cell_guid": "9aed0c5a-2c8a-4ad7-becb-60281923a196"
|
"azdata_cell_guid": "9aed0c5a-2c8a-4ad7-becb-60281923a196"
|
||||||
@@ -247,4 +243,4 @@
|
|||||||
"execution_count": null
|
"execution_count": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -25,12 +25,12 @@
|
|||||||
"source": [
|
"source": [
|
||||||
"\n",
|
"\n",
|
||||||
" \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",
|
" \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",
|
" \n",
|
||||||
"* Follow the instructions in the **Prerequisites** cell to install the tools if not already installed.\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",
|
"\n",
|
||||||
"<span style=\"color:red\"><font size=\"3\">Please press the \"Run All\" button to run the notebook</font></span>"
|
"<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",
|
"cell_type": "markdown",
|
||||||
"source": [
|
"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": {
|
"metadata": {
|
||||||
"azdata_cell_guid": "68531b91-ddce-47d7-a1d8-2ddc3d17f3e7"
|
"azdata_cell_guid": "68531b91-ddce-47d7-a1d8-2ddc3d17f3e7"
|
||||||
@@ -75,80 +89,20 @@
|
|||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"source": [
|
"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": {
|
"metadata": {
|
||||||
"azdata_cell_guid": "68ec0760-27d1-4ded-9a9f-89077c40b8bb"
|
"azdata_cell_guid": "68ec0760-27d1-4ded-9a9f-89077c40b8bb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"source": [
|
|
||||||
"# Required Values\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",
|
|
||||||
"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",
|
|
||||||
"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",
|
"cell_type": "markdown",
|
||||||
"source": [
|
"source": [
|
||||||
"#### **Get optional parameters for the PostgreSQL server group**"
|
"### **Creating the PostgreSQL Hyperscale - Azure Arc instance**"
|
||||||
],
|
|
||||||
"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**"
|
|
||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"azdata_cell_guid": "90b0e162-2987-463f-9ce6-12dda1267189"
|
"azdata_cell_guid": "90b0e162-2987-463f-9ce6-12dda1267189"
|
||||||
@@ -157,17 +111,37 @@
|
|||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"source": [
|
"source": [
|
||||||
"print (f'Creating a PostgreSQL server group on Azure Arc')\n",
|
"# Login to the data controller.\n",
|
||||||
|
"#\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()"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "71366399-5963-4e24-b2f2-6bb5bffba4ec"
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"print (f'Creating the PostgreSQL Hyperscale - Azure Arc instance')\n",
|
||||||
"\n",
|
"\n",
|
||||||
"workers_option = f' -w {server_group_workers}' if server_group_workers else \"\"\n",
|
"workers_option = f' -w {postgres_server_group_workers}' if postgres_server_group_workers else \"\"\n",
|
||||||
"port_option = f' --port \"{server_group_port}\"' if server_group_port else \"\"\n",
|
"port_option = f' --port \"{postgres_server_group_port}\"' if postgres_server_group_port else \"\"\n",
|
||||||
"cores_request_option = f' -cr \"{server_group_cores_request}\"' if server_group_cores_request else \"\"\n",
|
"engine_version_option = f' -ev {postgres_server_group_engine_version}' if postgres_server_group_engine_version else \"\"\n",
|
||||||
"cores_limit_option = f' -cl \"{server_group_cores_limit}\"' if server_group_cores_limit else \"\"\n",
|
"extensions_option = f' --extensions \"{postgres_server_group_extensions}\"' if postgres_server_group_extensions else \"\"\n",
|
||||||
"memory_request_option = f' -mr \"{server_group_memory_request}Mi\"' if server_group_memory_request else \"\"\n",
|
"volume_size_data_option = f' -vsd {postgres_server_group_volume_size_data}Gi' if postgres_server_group_volume_size_data else \"\"\n",
|
||||||
"memory_limit_option = f' -ml \"{server_group_memory_limit}Mi\"' if server_group_memory_limit 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",
|
"\n",
|
||||||
"os.environ[\"AZDATA_PASSWORD\"] = postgres_password\n",
|
"os.environ[\"AZDATA_PASSWORD\"] = os.environ[\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_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",
|
"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()"
|
"out=run_command()"
|
||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
@@ -177,4 +151,4 @@
|
|||||||
"execution_count": null
|
"execution_count": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -25,12 +25,12 @@
|
|||||||
"source": [
|
"source": [
|
||||||
"\n",
|
"\n",
|
||||||
" \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",
|
" \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",
|
" \n",
|
||||||
"* Follow the instructions in the **Prerequisites** cell to install the tools if not already installed.\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",
|
"\n",
|
||||||
"<span style=\"color:red\"><font size=\"3\">Please press the \"Run All\" button to run the notebook</font></span>"
|
"<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",
|
"cell_type": "markdown",
|
||||||
"source": [
|
"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": {
|
"metadata": {
|
||||||
"azdata_cell_guid": "68531b91-ddce-47d7-a1d8-2ddc3d17f3e7"
|
"azdata_cell_guid": "68531b91-ddce-47d7-a1d8-2ddc3d17f3e7"
|
||||||
@@ -75,49 +89,20 @@
|
|||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"source": [
|
"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": {
|
"metadata": {
|
||||||
"azdata_cell_guid": "68ec0760-27d1-4ded-9a9f-89077c40b8bb"
|
"azdata_cell_guid": "68ec0760-27d1-4ded-9a9f-89077c40b8bb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"source": [
|
|
||||||
"# Required Values\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",
|
|
||||||
"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",
|
|
||||||
"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",
|
|
||||||
"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",
|
"cell_type": "markdown",
|
||||||
"source": [
|
"source": [
|
||||||
"### **Installing Managed SQL Instance**"
|
"### **Creating the SQL managed instance - Azure Arc instance**"
|
||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"azdata_cell_guid": "90b0e162-2987-463f-9ce6-12dda1267189"
|
"azdata_cell_guid": "90b0e162-2987-463f-9ce6-12dda1267189"
|
||||||
@@ -126,10 +111,31 @@
|
|||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"source": [
|
"source": [
|
||||||
"print (f'Creating Managed SQL Server instance on Azure Arc')\n",
|
"# Login to the data controller.\n",
|
||||||
|
"#\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()"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "1437c536-17e8-4a7f-80c1-aa43ad02686c"
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"print (f'Creating the SQL managed instance - Azure Arc instance')\n",
|
||||||
"\n",
|
"\n",
|
||||||
"os.environ[\"AZDATA_PASSWORD\"] = mssql_password\n",
|
"cores_request_option = f' -cr \"{sql_cores_request}\"' if sql_cores_request else \"\"\n",
|
||||||
"cmd = f'azdata arc sql mi create -n {mssql_instance_name} -scd {mssql_storage_class_data} -scl {mssql_storage_class_logs}'\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()"
|
"out=run_command()"
|
||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
@@ -139,4 +145,4 @@
|
|||||||
"execution_count": null
|
"execution_count": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "arc",
|
"name": "arc",
|
||||||
"displayName": "%arc.displayName%",
|
"displayName": "%arc.displayName%",
|
||||||
"description": "%arc.description%",
|
"description": "%arc.description%",
|
||||||
"version": "0.3.5",
|
"version": "0.5.1",
|
||||||
"publisher": "Microsoft",
|
"publisher": "Microsoft",
|
||||||
"preview": true,
|
"preview": true,
|
||||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
|
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
|
||||||
@@ -14,10 +14,12 @@
|
|||||||
"activationEvents": [
|
"activationEvents": [
|
||||||
"onCommand:arc.connectToController",
|
"onCommand:arc.connectToController",
|
||||||
"onCommand:arc.createController",
|
"onCommand:arc.createController",
|
||||||
|
"onCommand:azdata.resource.deploy",
|
||||||
"onView:azureArc"
|
"onView:azureArc"
|
||||||
],
|
],
|
||||||
"extensionDependencies": [
|
"extensionDependencies": [
|
||||||
"Microsoft.azdata"
|
"Microsoft.azdata",
|
||||||
|
"Microsoft.resource-deployment"
|
||||||
],
|
],
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -96,7 +98,7 @@
|
|||||||
"view/item/context": [
|
"view/item/context": [
|
||||||
{
|
{
|
||||||
"command": "arc.openDashboard",
|
"command": "arc.openDashboard",
|
||||||
"when": "view == azureArc && viewItem != postgresInstances",
|
"when": "view == azureArc && viewItem",
|
||||||
"group": "navigation@1"
|
"group": "navigation@1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -138,79 +140,56 @@
|
|||||||
"light": "./images/data_controller.svg",
|
"light": "./images/data_controller.svg",
|
||||||
"dark": "./images/data_controller.svg"
|
"dark": "./images/data_controller.svg"
|
||||||
},
|
},
|
||||||
|
"tags": ["Hybrid", "SQL Server", "PostgreSQL"],
|
||||||
"providers": [
|
"providers": [
|
||||||
{
|
{
|
||||||
"notebookWizard": {
|
"notebookWizard": {
|
||||||
"notebook": "./notebooks/arcDeployment/deploy.arc.control.plane.ipynb",
|
"notebook": "./notebooks/arcDeployment/deploy.arc.data.controller.ipynb",
|
||||||
"type": "new-arc-control-plane",
|
"type": "new-arc-control-plane",
|
||||||
"runNotebook": false,
|
"doneAction": {
|
||||||
|
"label": "%deploy.done.action%"
|
||||||
|
},
|
||||||
|
"scriptAction": {
|
||||||
|
"label": "%deploy.script.action%"
|
||||||
|
},
|
||||||
"codeCellInsertionPosition": 5,
|
"codeCellInsertionPosition": 5,
|
||||||
"actionText": "%deploy.arc.control.plane.action%",
|
"title": "%arc.data.controller.new.wizard.title%",
|
||||||
"title": "%arc.control.plane.new.wizard.title%",
|
"name": "arc.data.controller.new.wizard",
|
||||||
"name": "arc.control.plane.new.wizard",
|
|
||||||
"labelPosition": "left",
|
"labelPosition": "left",
|
||||||
"generateSummaryPage": false,
|
"generateSummaryPage": false,
|
||||||
"pages": [
|
"pages": [
|
||||||
{
|
{
|
||||||
"title": "%arc.control.plane.select.cluster.title%",
|
"title": "%arc.data.controller.select.cluster.title%",
|
||||||
"sections": [
|
"sections": [
|
||||||
{
|
{
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"type": "kube_cluster_context_picker",
|
"type": "kube_cluster_context_picker",
|
||||||
"label": "%arc.control.plane.kube.cluster.context%",
|
"label": "%arc.data.controller.kube.cluster.context%",
|
||||||
"required": true,
|
"required": true,
|
||||||
"inputWidth": "350px",
|
"inputWidth": "350px",
|
||||||
"variableName": "AZDATA_NB_VAR_ARC_CLUSTER_CONTEXT",
|
"variableName": "AZDATA_NB_VAR_ARC_CLUSTER_CONTEXT",
|
||||||
"configFileVariableName": "AZDATA_NB_VAR_ARC_CONFIG_FILE"
|
"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": [
|
"sections": [
|
||||||
{
|
{
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"type": "options",
|
"type": "options",
|
||||||
"label": "%arc.control.plane.cluster.config.profile%",
|
"label": "%arc.data.controller.cluster.config.profile%",
|
||||||
"required": true,
|
"required": true,
|
||||||
"variableName": "AZDATA_NB_VAR_ARC_PROFILE",
|
"variableName": "AZDATA_NB_VAR_ARC_PROFILE",
|
||||||
"editable": false,
|
"editable": false,
|
||||||
"options": {
|
"options": {
|
||||||
"values": [
|
"source": {
|
||||||
"azure-arc-ake",
|
"providerId": "arc.controller.config.profiles"
|
||||||
"azure-arc-aks-default-storage",
|
},
|
||||||
"azure-arc-aks-premium-storage",
|
|
||||||
"azure-arc-azure-openshift",
|
|
||||||
"azure-arc-eks",
|
|
||||||
"azure-arc-kubeadm",
|
|
||||||
"azure-arc-openshift"
|
|
||||||
],
|
|
||||||
"defaultValue": "azure-arc-aks-default-storage",
|
"defaultValue": "azure-arc-aks-default-storage",
|
||||||
"optionsType": "radio"
|
"optionsType": "radio"
|
||||||
}
|
}
|
||||||
@@ -220,14 +199,14 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "%arc.control.plane.data.controller.create.title%",
|
"title": "%arc.data.controller.data.controller.create.title%",
|
||||||
"sections": [
|
"sections": [
|
||||||
{
|
{
|
||||||
"title": "%arc.control.plane.project.details.title%",
|
"title": "%arc.data.controller.project.details.title%",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"type": "readonly_text",
|
"type": "readonly_text",
|
||||||
"label": "%arc.control.plane.project.details.description%",
|
"label": "%arc.data.controller.project.details.description%",
|
||||||
"labelWidth": "600px"
|
"labelWidth": "600px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -240,30 +219,30 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "%arc.control.plane.data.controller.details.title%",
|
"title": "%arc.data.controller.data.controller.details.title%",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"type": "readonly_text",
|
"type": "readonly_text",
|
||||||
"label": "%arc.control.plane.data.controller.details.description%",
|
"label": "%arc.data.controller.data.controller.details.description%",
|
||||||
"labelWidth": "600px"
|
"labelWidth": "600px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"label": "%arc.control.plane.arc.data.controller.namespace%",
|
"label": "%arc.data.controller.arc.data.controller.namespace%",
|
||||||
"textValidationRequired": true,
|
"textValidationRequired": true,
|
||||||
"textValidationRegex": "^[a-z0-9]([-a-z0-9]{0,11}[a-z0-9])?$",
|
"textValidationRegex": "^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?$",
|
||||||
"textValidationDescription": "%arc.control.plane.arc.data.controller.namespace.validation.description%",
|
"textValidationDescription": "%arc.data.controller.arc.data.controller.namespace.validation.description%",
|
||||||
"defaultValue": "arc",
|
"defaultValue": "arc",
|
||||||
"required": true,
|
"required": true,
|
||||||
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAMESPACE"
|
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAMESPACE"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"label": "%arc.control.plane.arc.data.controller.name%",
|
"label": "%arc.data.controller.arc.data.controller.name%",
|
||||||
"textValidationRequired": true,
|
"textValidationRequired": true,
|
||||||
"textValidationRegex": "^[a-z0-9]([-a-z0-9]{0,11}[a-z0-9])?$",
|
"textValidationRegex": "^[a-z0-9]([-.a-z0-9]{0,251}[a-z0-9])?$",
|
||||||
"textValidationDescription": "%arc.control.plane.arc.data.controller.name.validation.description%",
|
"textValidationDescription": "%arc.data.controller.arc.data.controller.name.validation.description%",
|
||||||
"defaultValue": "arc-cp1",
|
"defaultValue": "arc-dc",
|
||||||
"required": true,
|
"required": true,
|
||||||
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAME"
|
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAME"
|
||||||
},
|
},
|
||||||
@@ -276,42 +255,33 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "azure_locations",
|
"type": "azure_locations",
|
||||||
"label": "%arc.control.plane.arc.data.controller.location%",
|
"label": "%arc.data.controller.arc.data.controller.location%",
|
||||||
"defaultValue": "eastus",
|
"defaultValue": "eastus",
|
||||||
"required": true,
|
"required": true,
|
||||||
"locationVariableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_LOCATION",
|
"locationVariableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_LOCATION",
|
||||||
"displayLocationVariableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_DISPLAY_LOCATION",
|
"displayLocationVariableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_DISPLAY_LOCATION",
|
||||||
"locations": [
|
"locations": [
|
||||||
|
"australiaeast",
|
||||||
|
"centralus",
|
||||||
"eastus",
|
"eastus",
|
||||||
"eastus2",
|
"eastus2",
|
||||||
"centralus",
|
"francecentral",
|
||||||
"westus2",
|
"japaneast",
|
||||||
|
"koreacentral",
|
||||||
|
"northeurope",
|
||||||
"southeastasia",
|
"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": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"label": "%arc.control.plane.admin.account.name%",
|
"label": "%arc.data.controller.admin.account.name%",
|
||||||
"variableName": "AZDATA_NB_VAR_ARC_ADMIN_USERNAME",
|
"variableName": "AZDATA_NB_VAR_ARC_ADMIN_USERNAME",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"required": true,
|
"required": true,
|
||||||
@@ -319,12 +289,12 @@
|
|||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "%arc.control.plane.admin.account.password%",
|
"label": "%arc.data.controller.admin.account.password%",
|
||||||
"variableName": "AZDATA_NB_VAR_ARC_ADMIN_PASSWORD",
|
"variableName": "AZDATA_NB_VAR_ARC_ADMIN_PASSWORD",
|
||||||
"type": "sql_password",
|
"type": "sql_password",
|
||||||
"userName": "arcadmin",
|
"userName": "arcadmin",
|
||||||
"confirmationRequired": true,
|
"confirmationRequired": true,
|
||||||
"confirmationLabel": "%arc.control.plane.admin.account.confirm.password%",
|
"confirmationLabel": "%arc.data.controller.admin.account.confirm.password%",
|
||||||
"defaultValue": "",
|
"defaultValue": "",
|
||||||
"required": true
|
"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,
|
"isSummaryPage": true,
|
||||||
"fieldHeight": "16px",
|
"fieldHeight": "16px",
|
||||||
"sections": [
|
"sections": [
|
||||||
@@ -349,7 +319,7 @@
|
|||||||
{
|
{
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"label": "%arc.control.plane.summary.arc.data.controller%",
|
"label": "%arc.data.controller.summary.arc.data.controller%",
|
||||||
"type": "readonly_text",
|
"type": "readonly_text",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"labelWidth": "185px"
|
"labelWidth": "185px"
|
||||||
@@ -359,7 +329,7 @@
|
|||||||
{
|
{
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"label": "%arc.control.plane.summary.estimated.cost.per.month%",
|
"label": "%arc.data.controller.summary.estimated.cost.per.month%",
|
||||||
"type": "readonly_text",
|
"type": "readonly_text",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"labelWidth": "190px",
|
"labelWidth": "190px",
|
||||||
@@ -376,7 +346,7 @@
|
|||||||
{
|
{
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"label": "%arc.control.plane.summary.arc.by.microsoft%",
|
"label": "%arc.data.controller.summary.arc.by.microsoft%",
|
||||||
"type": "readonly_text",
|
"type": "readonly_text",
|
||||||
"labelWidth": "185px"
|
"labelWidth": "185px"
|
||||||
}
|
}
|
||||||
@@ -385,7 +355,7 @@
|
|||||||
{
|
{
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"label": "%arc.control.plane.summary.free%",
|
"label": "%arc.data.controller.summary.free%",
|
||||||
"type": "readonly_text",
|
"type": "readonly_text",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"defaultValue": "",
|
"defaultValue": "",
|
||||||
@@ -403,10 +373,10 @@
|
|||||||
"label": "{0}",
|
"label": "{0}",
|
||||||
"type": "readonly_text",
|
"type": "readonly_text",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"labelWidth": "67px",
|
"labelWidth": "69px",
|
||||||
"links": [
|
"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"
|
"url": "https://go.microsoft.com/fwlink/?linkid=2045708"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -423,10 +393,10 @@
|
|||||||
"label": "{0}",
|
"label": "{0}",
|
||||||
"type": "readonly_text",
|
"type": "readonly_text",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"labelWidth": "102px",
|
"labelWidth": "100px",
|
||||||
"links": [
|
"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"
|
"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",
|
"fieldHeight": "88px",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"label": "%arc.control.plane.summary.terms.description%",
|
"label": "%arc.data.controller.summary.terms.description%",
|
||||||
"type": "readonly_text",
|
"type": "readonly_text",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"labelWidth": "750px",
|
"labelWidth": "750px",
|
||||||
"links": [
|
"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"
|
"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": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"label": "%arc.control.plane.summary.kube.config.file.path%",
|
"label": "%arc.data.controller.summary.kube.config.file.path%",
|
||||||
"type": "readonly_text",
|
"type": "readonly_text",
|
||||||
"isEvaluated": true,
|
"isEvaluated": true,
|
||||||
"defaultValue": "$(AZDATA_NB_VAR_ARC_CONFIG_FILE)"
|
"defaultValue": "$(AZDATA_NB_VAR_ARC_CONFIG_FILE)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "%arc.control.plane.summary.cluster.context%",
|
"label": "%arc.data.controller.summary.cluster.context%",
|
||||||
"type": "readonly_text",
|
"type": "readonly_text",
|
||||||
"isEvaluated": true,
|
"isEvaluated": true,
|
||||||
"defaultValue": "$(AZDATA_NB_VAR_ARC_CLUSTER_CONTEXT)"
|
"defaultValue": "$(AZDATA_NB_VAR_ARC_CLUSTER_CONTEXT)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "%arc.control.plane.summary.profile%",
|
"label": "%arc.data.controller.summary.profile%",
|
||||||
"type": "readonly_text",
|
"type": "readonly_text",
|
||||||
"isEvaluated": true,
|
"isEvaluated": true,
|
||||||
"defaultValue": "$(AZDATA_NB_VAR_ARC_PROFILE)"
|
"defaultValue": "$(AZDATA_NB_VAR_ARC_PROFILE)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "%arc.control.plane.summary.username%",
|
"label": "%arc.data.controller.summary.username%",
|
||||||
"type": "readonly_text",
|
"type": "readonly_text",
|
||||||
"isEvaluated": true,
|
"isEvaluated": true,
|
||||||
"defaultValue": "$(AZDATA_NB_VAR_ARC_ADMIN_USERNAME)"
|
"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": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"label": "%arc.control.plane.summary.data.controller.namespace%",
|
"label": "%arc.data.controller.summary.data.controller.namespace%",
|
||||||
"type": "readonly_text",
|
"type": "readonly_text",
|
||||||
"isEvaluated": true,
|
"isEvaluated": true,
|
||||||
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAMESPACE)"
|
"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",
|
"type": "readonly_text",
|
||||||
"isEvaluated": true,
|
"isEvaluated": true,
|
||||||
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAME)"
|
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAME)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "%arc.control.plane.summary.data.controller.connectivity.mode%",
|
"label": "%arc.data.controller.summary.subscription%",
|
||||||
"type": "readonly_text",
|
|
||||||
"isEvaluated": true,
|
|
||||||
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_CONNECTIVITY_MODE)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "%arc.control.plane.summary.subscription%",
|
|
||||||
"type": "readonly_text",
|
"type": "readonly_text",
|
||||||
"isEvaluated": true,
|
"isEvaluated": true,
|
||||||
"defaultValue": "$(AZDATA_NB_VAR_ARC_DISPLAY_SUBSCRIPTION)",
|
"defaultValue": "$(AZDATA_NB_VAR_ARC_DISPLAY_SUBSCRIPTION)",
|
||||||
"inputWidth": "600"
|
"inputWidth": "600"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "%arc.control.plane.summary.resource.group%",
|
"label": "%arc.data.controller.summary.resource.group%",
|
||||||
"type": "readonly_text",
|
"type": "readonly_text",
|
||||||
"isEvaluated": true,
|
"isEvaluated": true,
|
||||||
"defaultValue": "$(AZDATA_NB_VAR_ARC_RESOURCE_GROUP)"
|
"defaultValue": "$(AZDATA_NB_VAR_ARC_RESOURCE_GROUP)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "%arc.control.plane.summary.location%",
|
"label": "%arc.data.controller.summary.location%",
|
||||||
"type": "readonly_text",
|
"type": "readonly_text",
|
||||||
"isEvaluated": true,
|
"isEvaluated": true,
|
||||||
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_DISPLAY_LOCATION)"
|
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_DISPLAY_LOCATION)"
|
||||||
@@ -542,25 +500,12 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "azdata",
|
"name": "azdata",
|
||||||
"version": "20.1.0"
|
"version": "20.2.0"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"when": true
|
"when": true
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"agreement": {
|
|
||||||
"template": "%arc.control.plane.arc.data.controller.agreement%",
|
|
||||||
"links": [
|
|
||||||
{
|
|
||||||
"text": "%microsoft.agreement.privacy.statement%",
|
|
||||||
"url": "https://go.microsoft.com/fwlink/?LinkId=853010"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "%arc.agreement.azdata.eula%",
|
|
||||||
"url": "https://aka.ms/eula-azdata-en"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "arc.sql",
|
"name": "arc.sql",
|
||||||
@@ -571,48 +516,69 @@
|
|||||||
"light": "./images/miaa.svg",
|
"light": "./images/miaa.svg",
|
||||||
"dark": "./images/miaa.svg"
|
"dark": "./images/miaa.svg"
|
||||||
},
|
},
|
||||||
"options": [
|
"tags": ["Hybrid", "SQL Server"],
|
||||||
{
|
|
||||||
"name": "resourceType",
|
|
||||||
"displayName": "%resource.type.picker.display.name%",
|
|
||||||
"values": [
|
|
||||||
{
|
|
||||||
"name": "sql.managed.instance",
|
|
||||||
"displayName": "%sql.managed.instance.display.name%"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"providers": [
|
"providers": [
|
||||||
{
|
{
|
||||||
"dialog": {
|
"notebookWizard": {
|
||||||
"notebook": "./notebooks/arcDeployment/deploy.sql.existing.arc.ipynb",
|
"notebook": "./notebooks/arcDeployment/deploy.sql.existing.arc.ipynb",
|
||||||
"runNotebook": true,
|
"doneAction": {
|
||||||
"taskName": "%arc.sql.new.dialog.title%",
|
"label": "%deploy.done.action%"
|
||||||
"actionText": "%arc.deploy.action%",
|
},
|
||||||
"title": "%arc.sql.new.dialog.title%",
|
"scriptAction": {
|
||||||
"name": "arc.sql.new.dialog",
|
"label": "%deploy.script.action%"
|
||||||
"tabs": [
|
},
|
||||||
|
"codeCellInsertionPosition": 5,
|
||||||
|
"title": "%arc.sql.wizard.title%",
|
||||||
|
"name": "arc.sql.wizard",
|
||||||
|
"labelPosition": "left",
|
||||||
|
"generateSummaryPage": false,
|
||||||
|
"pages": [
|
||||||
{
|
{
|
||||||
"title": "",
|
"title": "%arc.sql.wizard.page1.title%",
|
||||||
|
"labelWidth": "175px",
|
||||||
|
"inputWidth": "280px",
|
||||||
"sections": [
|
"sections": [
|
||||||
{
|
{
|
||||||
"title": "%arc.sql.settings.section.title%",
|
"title": "%arc.sql.connection.settings.section.title%",
|
||||||
"fields": [
|
"fields": [
|
||||||
|
{
|
||||||
|
"label": "%arc.controller%",
|
||||||
|
"variableName": "",
|
||||||
|
"type": "options",
|
||||||
|
"editable": false,
|
||||||
|
"required": true,
|
||||||
|
"options": {
|
||||||
|
"source": {
|
||||||
|
"providerId": "arc.controllers",
|
||||||
|
"variableNames": {
|
||||||
|
"endpoint": "AZDATA_NB_VAR_CONTROLLER_ENDPOINT",
|
||||||
|
"username": "AZDATA_NB_VAR_CONTROLLER_USERNAME",
|
||||||
|
"password": "AZDATA_NB_VAR_CONTROLLER_PASSWORD"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"optionsType": "dropdown"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "%arc.sql.instance.name%",
|
"label": "%arc.sql.instance.name%",
|
||||||
"variableName": "AZDATA_NB_VAR_SQL_INSTANCE_NAME",
|
"variableName": "AZDATA_NB_VAR_SQL_INSTANCE_NAME",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"defaultValue": "sqlinstance1",
|
"defaultValue": "sqlinstance1",
|
||||||
|
"description": "%arc.sql.invalid.instance.name%",
|
||||||
"required": true,
|
"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%",
|
"label": "%arc.sql.username%",
|
||||||
"variableName": "AZDATA_NB_VAR_SQL_USERNAME",
|
"variableName": "AZDATA_NB_VAR_SQL_USERNAME",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"defaultValue": "sa",
|
"description": "%arc.sql.invalid.username%",
|
||||||
"enabled": false
|
"required": true,
|
||||||
|
"textValidationRequired": true,
|
||||||
|
"textValidationRegex": "^(?!sa$)",
|
||||||
|
"textValidationDescription": "%arc.sql.invalid.username%"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "%arc.password%",
|
"label": "%arc.password%",
|
||||||
@@ -623,7 +589,12 @@
|
|||||||
"confirmationLabel": "%arc.confirm.password%",
|
"confirmationLabel": "%arc.confirm.password%",
|
||||||
"defaultValue": "",
|
"defaultValue": "",
|
||||||
"required": true
|
"required": true
|
||||||
},
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "%arc.sql.instance.settings.section.title%",
|
||||||
|
"fields": [
|
||||||
{
|
{
|
||||||
"label": "%arc.storage-class.data.label%",
|
"label": "%arc.storage-class.data.label%",
|
||||||
"description": "%arc.sql.storage-class.data.description%",
|
"description": "%arc.sql.storage-class.data.description%",
|
||||||
@@ -637,6 +608,38 @@
|
|||||||
"variableName": "AZDATA_NB_VAR_SQL_STORAGE_CLASS_LOGS",
|
"variableName": "AZDATA_NB_VAR_SQL_STORAGE_CLASS_LOGS",
|
||||||
"type": "kube_storage_class",
|
"type": "kube_storage_class",
|
||||||
"required": true
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -650,10 +653,10 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "azdata",
|
"name": "azdata",
|
||||||
"version": "20.1.0"
|
"version": "20.2.0"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"when": "resourceType=sql.managed.instance"
|
"when": "true"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"agreement": {
|
"agreement": {
|
||||||
@@ -666,10 +669,6 @@
|
|||||||
{
|
{
|
||||||
"text": "%arc.agreement.sql.terms.conditions%",
|
"text": "%arc.agreement.sql.terms.conditions%",
|
||||||
"url": "https://go.microsoft.com/fwlink/?linkid=2045708"
|
"url": "https://go.microsoft.com/fwlink/?linkid=2045708"
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "%arc.agreement.azdata.eula%",
|
|
||||||
"url": "https://aka.ms/eula-azdata-en"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -683,42 +682,57 @@
|
|||||||
"light": "./images/postgres.svg",
|
"light": "./images/postgres.svg",
|
||||||
"dark": "./images/postgres.svg"
|
"dark": "./images/postgres.svg"
|
||||||
},
|
},
|
||||||
"options": [
|
"tags": ["Hybrid", "PostgreSQL"],
|
||||||
{
|
|
||||||
"name": "resourceType",
|
|
||||||
"displayName": "%resource.type.picker.display.name%",
|
|
||||||
"values": [
|
|
||||||
{
|
|
||||||
"name": "postgres",
|
|
||||||
"displayName": "%postgres.server.group.display.name%"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"providers": [
|
"providers": [
|
||||||
{
|
{
|
||||||
"dialog": {
|
"notebookWizard": {
|
||||||
"notebook": "./notebooks/arcDeployment/deploy.postgres.existing.arc.ipynb",
|
"notebook": "./notebooks/arcDeployment/deploy.postgres.existing.arc.ipynb",
|
||||||
"runNotebook": true,
|
"doneAction": {
|
||||||
"taskName": "%arc.postgres.new.dialog.title%",
|
"label": "%deploy.done.action%"
|
||||||
"actionText": "%arc.deploy.action%",
|
},
|
||||||
"title": "%arc.postgres.new.dialog.title%",
|
"scriptAction": {
|
||||||
"name": "arc.postgres.new.dialog",
|
"label": "%deploy.script.action%"
|
||||||
"tabs": [
|
},
|
||||||
|
"codeCellInsertionPosition": 5,
|
||||||
|
"title": "%arc.postgres.wizard.title%",
|
||||||
|
"name": "arc.postgres.wizard",
|
||||||
|
"labelPosition": "left",
|
||||||
|
"generateSummaryPage": false,
|
||||||
|
"pages": [
|
||||||
{
|
{
|
||||||
"title": "",
|
"title": "%arc.postgres.wizard.page1.title%",
|
||||||
|
"labelWidth": "205px",
|
||||||
|
"inputWidth": "280px",
|
||||||
"sections": [
|
"sections": [
|
||||||
{
|
{
|
||||||
"title": "%arc.postgres.settings.section.title%",
|
"title": "%arc.postgres.settings.section.title%",
|
||||||
"fields": [
|
"fields": [
|
||||||
|
{
|
||||||
|
"label": "%arc.controller%",
|
||||||
|
"variableName": "",
|
||||||
|
"type": "options",
|
||||||
|
"editable": false,
|
||||||
|
"required": true,
|
||||||
|
"options": {
|
||||||
|
"source": {
|
||||||
|
"providerId": "arc.controllers",
|
||||||
|
"variableNames": {
|
||||||
|
"endpoint": "AZDATA_NB_VAR_CONTROLLER_ENDPOINT",
|
||||||
|
"username": "AZDATA_NB_VAR_CONTROLLER_USERNAME",
|
||||||
|
"password": "AZDATA_NB_VAR_CONTROLLER_PASSWORD"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"optionsType": "dropdown"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "%arc.postgres.server.group.name%",
|
"label": "%arc.postgres.server.group.name%",
|
||||||
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_NAME",
|
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_NAME",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
"description": "%arc.postgres.server.group.name.validation.description%",
|
||||||
"textValidationRequired": true,
|
"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%",
|
"textValidationDescription": "%arc.postgres.server.group.name.validation.description%",
|
||||||
"defaultValue": "postgres1",
|
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -731,11 +745,12 @@
|
|||||||
"required": true
|
"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",
|
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_WORKERS",
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"defaultValue": "1",
|
"defaultValue": "0",
|
||||||
"min": 1
|
"min": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "%arc.postgres.server.group.port%",
|
"label": "%arc.postgres.server.group.port%",
|
||||||
@@ -745,6 +760,27 @@
|
|||||||
"min": 1,
|
"min": 1,
|
||||||
"max": 65535
|
"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%",
|
"label": "%arc.storage-class.data.label%",
|
||||||
"description": "%arc.postgres.storage-class.data.description%",
|
"description": "%arc.postgres.storage-class.data.description%",
|
||||||
@@ -752,6 +788,14 @@
|
|||||||
"type": "kube_storage_class",
|
"type": "kube_storage_class",
|
||||||
"required": true
|
"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%",
|
"label": "%arc.storage-class.logs.label%",
|
||||||
"description": "%arc.postgres.storage-class.logs.description%",
|
"description": "%arc.postgres.storage-class.logs.description%",
|
||||||
@@ -759,12 +803,28 @@
|
|||||||
"type": "kube_storage_class",
|
"type": "kube_storage_class",
|
||||||
"required": true
|
"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%",
|
"label": "%arc.storage-class.backups.label%",
|
||||||
"description": "%arc.postgres.storage-class.backups.description%",
|
"description": "%arc.postgres.storage-class.backups.description%",
|
||||||
"variableName": "AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_BACKUPS",
|
"variableName": "AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_BACKUPS",
|
||||||
"type": "kube_storage_class",
|
"type": "kube_storage_class",
|
||||||
"required": true
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -772,28 +832,32 @@
|
|||||||
"title": "%arc.postgres.settings.resource.title%",
|
"title": "%arc.postgres.settings.resource.title%",
|
||||||
"fields": [
|
"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",
|
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_REQUEST",
|
||||||
"type": "number",
|
"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",
|
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_LIMIT",
|
||||||
"type": "number",
|
"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",
|
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_REQUEST",
|
||||||
"type": "number",
|
"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",
|
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_LIMIT",
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"min": 0
|
"min": 0.25
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -803,14 +867,14 @@
|
|||||||
},
|
},
|
||||||
"requiredTools": [
|
"requiredTools": [
|
||||||
{
|
{
|
||||||
"name": "azure-cli"
|
"name": "kubectl"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "azdata",
|
"name": "azdata",
|
||||||
"version": "20.1.0"
|
"version": "20.2.0"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"when": "resourceType=postgres"
|
"when": "true"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"agreement": {
|
"agreement": {
|
||||||
@@ -823,10 +887,6 @@
|
|||||||
{
|
{
|
||||||
"text": "%arc.agreement.postgres.terms.conditions%",
|
"text": "%arc.agreement.postgres.terms.conditions%",
|
||||||
"url": "https://go.microsoft.com/fwlink/?linkid=2045708"
|
"url": "https://go.microsoft.com/fwlink/?linkid=2045708"
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "%arc.agreement.azdata.eula%",
|
|
||||||
"url": "https://aka.ms/eula-azdata-en"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -851,5 +911,10 @@
|
|||||||
"sinon": "^9.0.2",
|
"sinon": "^9.0.2",
|
||||||
"typemoq": "2.1.0",
|
"typemoq": "2.1.0",
|
||||||
"vscodetestcover": "^1.1.0"
|
"vscodetestcover": "^1.1.0"
|
||||||
|
},
|
||||||
|
"__metadata": {
|
||||||
|
"id": "68",
|
||||||
|
"publisherDisplayName": "Microsoft",
|
||||||
|
"publisherId": "Microsoft"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,75 +12,71 @@
|
|||||||
"command.editConnection.title": "Edit Connection",
|
"command.editConnection.title": "Edit Connection",
|
||||||
"arc.openDashboard": "Manage",
|
"arc.openDashboard": "Manage",
|
||||||
|
|
||||||
"resource.type.azure.arc.display.name": "Azure Arc data controller",
|
"resource.type.azure.arc.display.name": "Azure Arc data controller (preview)",
|
||||||
"resource.type.azure.arc.description": "Creates an Azure Arc data controller",
|
"resource.type.azure.arc.description": "Creates an Azure Arc data controller",
|
||||||
|
"arc.data.controller.new.wizard.title": "Create Azure Arc data controller",
|
||||||
"arc.control.plane.new.wizard.title": "Create Azure Arc data controller",
|
"arc.data.controller.cluster.environment.title": "What is your target existing Kubernetes cluster environment?",
|
||||||
"arc.control.plane.cluster.environment.title": "What is your target existing Kubernetes cluster environment?",
|
"arc.data.controller.select.cluster.title": "Select from existing Kubernetes clusters",
|
||||||
"arc.control.plane.select.cluster.title": "Select from existing Kubernetes clusters",
|
"arc.data.controller.kube.cluster.context": "Cluster context",
|
||||||
"arc.control.plane.kube.cluster.context": "Cluster context",
|
"arc.data.controller.cluster.config.profile.title": "Choose the config profile",
|
||||||
"arc.control.plane.container.registry.title": "Container registry details",
|
"arc.data.controller.cluster.config.profile": "Config profile",
|
||||||
"arc.control.plane.container.registry.name": "Container registry login",
|
"arc.data.controller.data.controller.create.title": "Provide details to create Azure Arc data controller",
|
||||||
"arc.control.plane.container.registry.password": "Container registry password",
|
"arc.data.controller.project.details.title": "Project details",
|
||||||
"arc.control.plane.cluster.config.profile.title": "Choose the config profile",
|
"arc.data.controller.project.details.description": "Select the subscription to manage deployed resources and costs. Use resource groups like folders to organize and manage all your resources.",
|
||||||
"arc.control.plane.cluster.config.profile": "Config profile",
|
"arc.data.controller.data.controller.details.title": "Data controller details",
|
||||||
"arc.control.plane.data.controller.create.title": "Provide details to create Azure Arc data controller",
|
"arc.data.controller.data.controller.details.description": "Provide an Azure region and a name for your Azure Arc data controller. This name will be used to identify your Arc location for remote management and monitoring.",
|
||||||
"arc.control.plane.project.details.title": "Project details",
|
"arc.data.controller.arc.data.controller.namespace": "Data controller namespace",
|
||||||
"arc.control.plane.project.details.description": "Select the subscription to manage deployed resources and costs. Use resource groups like folders to organize and manage all your resources.",
|
"arc.data.controller.arc.data.controller.namespace.validation.description": "Namespace must consist of lower case alphanumeric characters or '-', start/end with an alphanumeric character, and be 63 characters or fewer in length.",
|
||||||
"arc.control.plane.data.controller.details.title": "Data controller details",
|
"arc.data.controller.arc.data.controller.name": "Data controller name",
|
||||||
"arc.control.plane.data.controller.details.description": "Provide an Azure region and a name for your Azure Arc data controller. This name will be used to identify your Arc location for remote management and monitoring.",
|
"arc.data.controller.arc.data.controller.name.validation.description": "Name must consist of lower case alphanumeric characters, '-' or '.', start/end with an alphanumeric character and be 253 characters or less in length.",
|
||||||
"arc.control.plane.arc.data.controller.connectivity.mode": "Data controller connectivity mode",
|
"arc.data.controller.arc.data.controller.location": "Location",
|
||||||
"arc.control.plane.arc.data.controller.namespace": "Data controller namespace",
|
"arc.data.controller.admin.account.title": "Administrator account",
|
||||||
"arc.control.plane.arc.data.controller.namespace.validation.description": "Data controller namespace (lower case letters, digits and - only)",
|
"arc.data.controller.admin.account.name": "Data controller login",
|
||||||
"arc.control.plane.arc.data.controller.name": "Data controller name",
|
"arc.data.controller.admin.account.password": "Password",
|
||||||
"arc.control.plane.arc.data.controller.name.validation.description": "Data controller name (lower case letters, digits and - only)",
|
"arc.data.controller.admin.account.confirm.password": "Confirm password",
|
||||||
"arc.control.plane.arc.data.controller.location": "Location",
|
"arc.data.controller.data.controller.create.summary.title": "Review your configuration",
|
||||||
"arc.control.plane.admin.account.title": "Administrator account",
|
"arc.data.controller.summary.arc.data.controller": "Azure Arc data controller",
|
||||||
"arc.control.plane.admin.account.name": "Data controller login",
|
"arc.data.controller.summary.estimated.cost.per.month": "Estimated cost per month",
|
||||||
"arc.control.plane.admin.account.password": "Password",
|
"arc.data.controller.summary.arc.by.microsoft" : "by Microsoft",
|
||||||
"arc.control.plane.admin.account.confirm.password": "Confirm password",
|
"arc.data.controller.summary.free" : "Free",
|
||||||
"arc.control.plane.data.controller.create.summary.title": "Review your configuration",
|
"arc.data.controller.summary.arc.terms.of.use" : "Terms of use",
|
||||||
"arc.control.plane.summary.arc.data.controller": "Azure Arc data controller",
|
"arc.data.controller.summary.arc.terms.separator" : "|",
|
||||||
"arc.control.plane.summary.estimated.cost.per.month": "Estimated cost per month",
|
"arc.data.controller.summary.arc.terms.privacy.policy" : "Privacy policy",
|
||||||
"arc.control.plane.summary.arc.by.microsoft" : "by Microsoft",
|
"arc.data.controller.summary.terms" : "Terms",
|
||||||
"arc.control.plane.summary.free" : "Free",
|
"arc.data.controller.summary.terms.description": "By clicking 'Script to notebook', I (a) agree to the legal terms and privacy statement(s) associated with the Marketplace offering(s) listed above; (b) authorize Microsoft to bill my current payment method for the fees associated with the offering(s), with the same billing frequency as my Azure subscription; and (c) agree that Microsoft may share my contact, usage and transactional information with the provider(s) of the offering(s) for support, billing and other transactional activities. Microsoft does not provide rights for third-party offerings. For additional details see {0}.",
|
||||||
"arc.control.plane.summary.arc.terms.of.use" : "Terms of use",
|
"arc.data.controller.summary.terms.link.text": "Azure Marketplace Terms",
|
||||||
"arc.control.plane.summary.arc.terms.separator" : "|",
|
"arc.data.controller.summary.kubernetes": "Kubernetes",
|
||||||
"arc.control.plane.summary.arc.terms.privacy.policy" : "Privacy policy",
|
"arc.data.controller.summary.kube.config.file.path": "Kube config file path",
|
||||||
"arc.control.plane.summary.terms" : "Terms",
|
"arc.data.controller.summary.cluster.context": "Cluster context",
|
||||||
"arc.control.plane.summary.terms.description": "By clicking 'Script to notebook', I (a) agree to the legal terms and privacy statement(s) associated with the Marketplace offering(s) listed above; (b) authorize Microsoft to bill my current payment method for the fees associated with the offering(s), with the same billing frequency as my Azure subscription; and (c) agree that Microsoft may share my contact, usage and transactional information with the provider(s) of the offering(s) for support, billing and other transactional activities. Microsoft does not provide rights for third-party offerings. For additional details see {0}.",
|
"arc.data.controller.summary.profile": "Config profile",
|
||||||
"arc.control.plane.summary.terms.link.text": "Azure Marketplace Terms",
|
"arc.data.controller.summary.username": "Username",
|
||||||
"arc.control.plane.summary.kubernetes": "Kubernetes",
|
"arc.data.controller.summary.azure": "Azure",
|
||||||
"arc.control.plane.summary.kube.config.file.path": "Kube config file path",
|
"arc.data.controller.summary.subscription": "Subscription",
|
||||||
"arc.control.plane.summary.cluster.context": "Cluster context",
|
"arc.data.controller.summary.resource.group": "Resource group",
|
||||||
"arc.control.plane.summary.profile": "Config profile",
|
"arc.data.controller.summary.data.controller.name": "Data controller name",
|
||||||
"arc.control.plane.summary.username": "Username",
|
"arc.data.controller.summary.data.controller.namespace": "Data controller namespace",
|
||||||
"arc.control.plane.summary.docker.username": "Docker username",
|
"arc.data.controller.summary.location": "Location",
|
||||||
"arc.control.plane.summary.azure": "Azure",
|
"arc.data.controller.arc.data.controller.agreement": "I accept {0} and {1}.",
|
||||||
"arc.control.plane.summary.subscription": "Subscription",
|
|
||||||
"arc.control.plane.summary.resource.group": "Resource group",
|
|
||||||
"arc.control.plane.summary.data.controller.connectivity.mode": "Data controller connectivity mode",
|
|
||||||
"arc.control.plane.summary.data.controller.name": "Data controller name",
|
|
||||||
"arc.control.plane.summary.data.controller.namespace": "Data controller namespace",
|
|
||||||
"arc.control.plane.summary.location": "Location",
|
|
||||||
"arc.control.plane.arc.data.controller.agreement": "I accept {0} and {1}.",
|
|
||||||
"microsoft.agreement.privacy.statement":"Microsoft Privacy Statement",
|
"microsoft.agreement.privacy.statement":"Microsoft Privacy Statement",
|
||||||
"arc.agreement.azdata.eula":"azdata license terms",
|
"deploy.script.action":"Script to notebook",
|
||||||
"deploy.arc.control.plane.action":"Script to notebook",
|
"deploy.done.action":"Deploy",
|
||||||
|
|
||||||
|
|
||||||
"resource.type.arc.sql.display.name": "Azure SQL managed instance - Azure Arc (preview)",
|
"resource.type.arc.sql.display.name": "Azure SQL managed instance - Azure Arc (preview)",
|
||||||
"resource.type.arc.postgres.display.name": "PostgreSQL server groups - Azure Arc (preview)",
|
"resource.type.arc.postgres.display.name": "PostgreSQL Hyperscale server groups - Azure Arc (preview)",
|
||||||
"resource.type.arc.sql.description": "Managed SQL Instance service for app developers in a customer-managed environment",
|
"resource.type.arc.sql.description": "Managed SQL Instance service for app developers in a customer-managed environment",
|
||||||
"resource.type.arc.postgres.description": "Deploy PostgreSQL server groups into an Azure Arc environment",
|
"resource.type.arc.postgres.description": "Deploy PostgreSQL Hyperscale server groups into an Azure Arc environment",
|
||||||
"resource.type.picker.display.name": "Resource Type",
|
"arc.controller": "Target Azure Arc Controller",
|
||||||
"sql.managed.instance.display.name": "Azure SQL managed instance - Azure Arc",
|
|
||||||
"postgres.server.group.display.name": "PostgreSQL server groups - Azure Arc",
|
|
||||||
"arc.sql.new.dialog.title": "Deploy Azure SQL managed instance - Azure Arc (preview)",
|
"arc.sql.wizard.title": "Deploy Azure SQL managed instance - Azure Arc (preview)",
|
||||||
"arc.sql.settings.section.title": "SQL Connection information",
|
"arc.sql.wizard.page1.title": "Provide Azure SQL managed instance parameters",
|
||||||
|
"arc.sql.connection.settings.section.title": "SQL Connection information",
|
||||||
|
"arc.sql.instance.settings.section.title": "SQL Instance settings",
|
||||||
"arc.azure.section.title": "Azure information",
|
"arc.azure.section.title": "Azure information",
|
||||||
"arc.sql.instance.name": "Instance name (lower case letters and digits only)",
|
"arc.sql.instance.name": "Instance name",
|
||||||
"arc.sql.username": "Username",
|
"arc.sql.username": "Username",
|
||||||
|
"arc.sql.invalid.username": "sa username is disabled, please choose another username",
|
||||||
|
"arc.sql.invalid.instance.name": "Instance name must consist of lower case alphanumeric characters or '-', start with a letter, end with an alphanumeric character, and be 13 characters or fewer in length.",
|
||||||
"arc.storage-class.dc.label": "Storage Class",
|
"arc.storage-class.dc.label": "Storage Class",
|
||||||
"arc.sql.storage-class.dc.description": "The storage class to be used for all data and logs persistent volumes for all data controller pods that require them.",
|
"arc.sql.storage-class.dc.description": "The storage class to be used for all data and logs persistent volumes for all data controller pods that require them.",
|
||||||
"arc.storage-class.data.label": "Storage Class (Data)",
|
"arc.storage-class.data.label": "Storage Class (Data)",
|
||||||
@@ -90,6 +86,14 @@
|
|||||||
"arc.sql.storage-class.logs.description": "The storage class to be used for logs (/var/log)",
|
"arc.sql.storage-class.logs.description": "The storage class to be used for logs (/var/log)",
|
||||||
"arc.postgres.storage-class.logs.description": "The storage class to be used for logs persistent volumes",
|
"arc.postgres.storage-class.logs.description": "The storage class to be used for logs persistent volumes",
|
||||||
"arc.storage-class.backups.label": "Storage Class (Backups)",
|
"arc.storage-class.backups.label": "Storage Class (Backups)",
|
||||||
|
"arc.cores-limit.label": "Cores Limit",
|
||||||
|
"arc.sql.cores-limit.description": "The cores limit of the managed instance as an integer.",
|
||||||
|
"arc.cores-request.label": "Cores Request",
|
||||||
|
"arc.sql.cores-request.description": "The request for cores of the managed instance as an integer.",
|
||||||
|
"arc.memory-limit.label": "Memory Limit",
|
||||||
|
"arc.sql.memory-limit.description": "The limit of the capacity of the managed instance as an integer.",
|
||||||
|
"arc.memory-request.label": "Memory Request",
|
||||||
|
"arc.sql.memory-request.description": "The request for the capacity of the managed instance as an integer amount of memory in GBs.",
|
||||||
"arc.postgres.storage-class.backups.description": "The storage class to be used for backup persistent volumes",
|
"arc.postgres.storage-class.backups.description": "The storage class to be used for backup persistent volumes",
|
||||||
"arc.password": "Password",
|
"arc.password": "Password",
|
||||||
"arc.confirm.password": "Confirm password",
|
"arc.confirm.password": "Confirm password",
|
||||||
@@ -97,19 +101,34 @@
|
|||||||
"arc.azure.subscription": "Azure subscription",
|
"arc.azure.subscription": "Azure subscription",
|
||||||
"arc.azure.resource.group": "Azure resource group",
|
"arc.azure.resource.group": "Azure resource group",
|
||||||
"arc.azure.location": "Azure location",
|
"arc.azure.location": "Azure location",
|
||||||
"arc.postgres.new.dialog.title": "Deploy a PostgreSQL server group on Azure Arc (preview)",
|
"arc.postgres.wizard.title": "Deploy an Azure Arc enabled PostgreSQL Hyperscale server group (Preview)",
|
||||||
"arc.postgres.settings.section.title": "PostgreSQL server group settings",
|
"arc.postgres.wizard.page1.title": "Provide Azure enabled PostgreSQL Hyperscale server group parameters",
|
||||||
"arc.postgres.settings.resource.title": "PostgreSQL server group resource settings",
|
"arc.postgres.settings.section.title": "General settings",
|
||||||
|
"arc.postgres.settings.resource.title": "Resource settings",
|
||||||
|
"arc.postgres.settings.storage.title": "Storage settings",
|
||||||
"arc.postgres.server.group.name": "Server group name",
|
"arc.postgres.server.group.name": "Server group name",
|
||||||
"arc.postgres.server.group.name.validation.description": "Server group name must consist of lower case alphanumeric characters or '-', start with a letter, end with an alphanumeric character, and be 10 characters or fewer in length.",
|
"arc.postgres.server.group.name.validation.description": "Server group name must consist of lower case alphanumeric characters or '-', start with a letter, end with an alphanumeric character, and be 12 characters or fewer in length.",
|
||||||
"arc.postgres.server.group.workers": "Number of workers",
|
"arc.postgres.server.group.workers.label": "Number of workers",
|
||||||
|
"arc.postgres.server.group.workers.description": "The number of worker nodes to provision in a sharded cluster, or zero (the default) for single-node Postgres.",
|
||||||
"arc.postgres.server.group.port": "Port",
|
"arc.postgres.server.group.port": "Port",
|
||||||
"arc.postgres.server.group.cores.request": "Min CPU cores (per node) to reserve",
|
"arc.postgres.server.group.engine.version": "Engine Version",
|
||||||
"arc.postgres.server.group.cores.limit": "Max CPU cores (per node) to allow",
|
"arc.postgres.server.group.extensions.label": "Extensions",
|
||||||
"arc.postgres.server.group.memory.request": "Min memory MB (per node) to reserve",
|
"arc.postgres.server.group.extensions.description": "A comma-separated list of the Postgres extensions that should be loaded on startup. Please refer to the postgres documentation for supported values.",
|
||||||
"arc.postgres.server.group.memory.limit": "Max memory MB (per node) to allow",
|
"arc.postgres.server.group.volume.size.data.label": "Volume Size GB (Data)",
|
||||||
"arc.agreement": "I accept {0}, {1} and {2}.",
|
"arc.postgres.server.group.volume.size.data.description": "The size of the storage volume to be used for data in GB.",
|
||||||
|
"arc.postgres.server.group.volume.size.logs.label": "Volume Size GB (Logs)",
|
||||||
|
"arc.postgres.server.group.volume.size.logs.description": "The size of the storage volume to be used for logs in GB.",
|
||||||
|
"arc.postgres.server.group.volume.size.backups.label": "Volume Size GB (Backups)",
|
||||||
|
"arc.postgres.server.group.volume.size.backups.description": "The size of the storage volume to be used for backups in GB.",
|
||||||
|
"arc.postgres.server.group.cores.request.label": "CPU request (cores per node)",
|
||||||
|
"arc.postgres.server.group.cores.request.description": "The minimum number of CPU cores that must be available per node to schedule the service. Fractional cores are supported.",
|
||||||
|
"arc.postgres.server.group.cores.limit.label": "CPU limit (cores per node)",
|
||||||
|
"arc.postgres.server.group.cores.limit.description": "The maximum number of CPU cores for the Postgres instance that can be used per node. Fractional cores are supported.",
|
||||||
|
"arc.postgres.server.group.memory.request.label": "Memory request (GB per node)",
|
||||||
|
"arc.postgres.server.group.memory.request.description": "The memory request of the Postgres instance per node in GB.",
|
||||||
|
"arc.postgres.server.group.memory.limit.label": "Memory limit (GB per node)",
|
||||||
|
"arc.postgres.server.group.memory.limit.description": "The memory limit of the Postgres instance per node in GB.",
|
||||||
|
"arc.agreement": "I accept {0} and {1}.",
|
||||||
"arc.agreement.sql.terms.conditions":"Azure SQL managed instance - Azure Arc terms and conditions",
|
"arc.agreement.sql.terms.conditions":"Azure SQL managed instance - Azure Arc terms and conditions",
|
||||||
"arc.agreement.postgres.terms.conditions":"PostgreSQL server groups - Azure Arc terms and conditions",
|
"arc.agreement.postgres.terms.conditions":"Azure Arc enabled PostgreSQL Hyperscale terms and conditions"
|
||||||
"arc.deploy.action":"Deploy"
|
|
||||||
}
|
}
|
||||||
|
|||||||
41
extensions/arc/src/common/api.ts
Normal file
41
extensions/arc/src/common/api.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import * as arc from 'arc';
|
||||||
|
import { PasswordToControllerDialog } from '../ui/dialogs/connectControllerDialog';
|
||||||
|
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
|
||||||
|
import { ControllerTreeNode } from '../ui/tree/controllerTreeNode';
|
||||||
|
import { UserCancelledError } from './utils';
|
||||||
|
|
||||||
|
export function arcApi(treeDataProvider: AzureArcTreeDataProvider): arc.IExtension {
|
||||||
|
return {
|
||||||
|
getRegisteredDataControllers: () => getRegisteredDataControllers(treeDataProvider),
|
||||||
|
getControllerPassword: (controllerInfo: arc.ControllerInfo) => getControllerPassword(treeDataProvider, controllerInfo),
|
||||||
|
reacquireControllerPassword: (controllerInfo: arc.ControllerInfo) => reacquireControllerPassword(treeDataProvider, controllerInfo)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export async function reacquireControllerPassword(treeDataProvider: AzureArcTreeDataProvider, controllerInfo: arc.ControllerInfo): Promise<string> {
|
||||||
|
const dialog = new PasswordToControllerDialog(treeDataProvider);
|
||||||
|
dialog.showDialog(controllerInfo);
|
||||||
|
const model = await dialog.waitForClose();
|
||||||
|
if (!model) {
|
||||||
|
throw new UserCancelledError();
|
||||||
|
}
|
||||||
|
return model.password;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getControllerPassword(treeDataProvider: AzureArcTreeDataProvider, controllerInfo: arc.ControllerInfo): Promise<string> {
|
||||||
|
return await treeDataProvider.getPassword(controllerInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRegisteredDataControllers(treeDataProvider: AzureArcTreeDataProvider): Promise<arc.DataController[]> {
|
||||||
|
return (await treeDataProvider.getChildren())
|
||||||
|
.filter(node => node instanceof ControllerTreeNode)
|
||||||
|
.map(node => ({
|
||||||
|
label: (node as ControllerTreeNode).model.label,
|
||||||
|
info: (node as ControllerTreeNode).model.info
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
83
extensions/arc/src/common/cacheManager.ts
Normal file
83
extensions/arc/src/common/cacheManager.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import { Deferred } from './promise';
|
||||||
|
|
||||||
|
const enum Status {
|
||||||
|
notStarted,
|
||||||
|
inProgress,
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State<T> {
|
||||||
|
entry?: T,
|
||||||
|
error?: Error,
|
||||||
|
status: Status,
|
||||||
|
id: number,
|
||||||
|
pendingOperation: Deferred<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of Cache Manager which ensures that only one call to populate cache miss is pending at a given time.
|
||||||
|
* All remaining calls for retrieval are awaited until the one in progress finishes and then all awaited calls are resolved with the value
|
||||||
|
* from the cache.
|
||||||
|
*/
|
||||||
|
export class CacheManager<K, T> {
|
||||||
|
private _cache = new Map<K, State<T>>();
|
||||||
|
private _id = 0;
|
||||||
|
|
||||||
|
public async getCacheEntry(key: K, retrieveEntry: (key: K) => Promise<T>): Promise<T> {
|
||||||
|
const cacheHit: State<T> | undefined = this._cache.get(key);
|
||||||
|
// each branch either throws or returns the password.
|
||||||
|
if (cacheHit === undefined) {
|
||||||
|
// populate a new state entry and add it to the cache
|
||||||
|
const state: State<T> = {
|
||||||
|
status: Status.notStarted,
|
||||||
|
id: this._id++,
|
||||||
|
pendingOperation: new Deferred<void>()
|
||||||
|
};
|
||||||
|
this._cache.set(key, state);
|
||||||
|
// now that we have the state entry initialized, retry to fetch the cacheEntry
|
||||||
|
let returnValue: T = await this.getCacheEntry(key, retrieveEntry);
|
||||||
|
await state.pendingOperation;
|
||||||
|
return returnValue!;
|
||||||
|
} else {
|
||||||
|
switch (cacheHit.status) {
|
||||||
|
case Status.notStarted: {
|
||||||
|
cacheHit.status = Status.inProgress;
|
||||||
|
// retrieve and populate the missed cache hit.
|
||||||
|
try {
|
||||||
|
cacheHit.entry = await retrieveEntry(key);
|
||||||
|
} catch (error) {
|
||||||
|
cacheHit.error = error;
|
||||||
|
} finally {
|
||||||
|
cacheHit.status = Status.done;
|
||||||
|
// we do not reject here even in error case because we do not want our awaits on pendingOperation to throw
|
||||||
|
// We track our own error state and when all done we throw if an error had happened. This results
|
||||||
|
// in the rejection of the promised returned by this method.
|
||||||
|
cacheHit.pendingOperation.resolve();
|
||||||
|
}
|
||||||
|
return await this.getCacheEntry(key, retrieveEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
case Status.inProgress: {
|
||||||
|
await cacheHit.pendingOperation;
|
||||||
|
return await this.getCacheEntry(key, retrieveEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
case Status.done: {
|
||||||
|
if (cacheHit.error !== undefined) {
|
||||||
|
await cacheHit.pendingOperation;
|
||||||
|
throw cacheHit.error;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await cacheHit.pendingOperation;
|
||||||
|
return cacheHit.entry!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
* Opens an input box prompting the user to enter in the name of an instance to delete
|
||||||
* @param name The name of the resource 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
|
* @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> {
|
export async function promptForInstanceDeletion(name: string): Promise<boolean> {
|
||||||
const title = loc.resourceDeletionWarning(name);
|
const title = loc.instanceDeletionWarning(name);
|
||||||
const options: vscode.InputBoxOptions = {
|
const options: vscode.InputBoxOptions = {
|
||||||
placeHolder: name,
|
placeHolder: name,
|
||||||
validateInput: input => input !== name ? loc.invalidResourceDeletionName(name) : ''
|
validateInput: input => input !== name ? loc.invalidInstanceDeletionName(name) : ''
|
||||||
};
|
};
|
||||||
|
|
||||||
return await promptInputBox(title, options) !== undefined;
|
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.
|
* Gets the message to display for a given error object that may be a variety of types.
|
||||||
* @param error The error object
|
* @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;
|
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>
|
* Parses an address into its separate ip and port values. Address must be in the form <ip>:<port>
|
||||||
* @param address The address to parse
|
* @param address The address to parse
|
||||||
@@ -225,3 +212,21 @@ export function parseIpAndPort(address: string): { ip: string, port: string } {
|
|||||||
port: sections[1]
|
port: sections[1]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createCredentialId(controllerId: string, resourceType: string, instanceName: string): string {
|
||||||
|
return `${controllerId}::${resourceType}::${instanceName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws an Error with given {@link message} unless {@link condition} is true.
|
||||||
|
* This also tells the typescript compiler that the condition is 'truthy' in the remainder of the scope
|
||||||
|
* where this function was called.
|
||||||
|
*
|
||||||
|
* @param condition
|
||||||
|
* @param message
|
||||||
|
*/
|
||||||
|
export function throwUnless(condition: boolean, message?: string): asserts condition {
|
||||||
|
if (!condition) {
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ import * as vscode from 'vscode';
|
|||||||
|
|
||||||
export const refreshActionId = 'arc.refresh';
|
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 {
|
export interface IconPath {
|
||||||
dark: string;
|
dark: string;
|
||||||
light: string;
|
light: string;
|
||||||
|
|||||||
@@ -4,9 +4,12 @@
|
|||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import * as arc from 'arc';
|
import * as arc from 'arc';
|
||||||
|
import * as rd from 'resource-deployment';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
|
import { arcApi } from './common/api';
|
||||||
import { IconPathHelper, refreshActionId } from './constants';
|
import { IconPathHelper, refreshActionId } from './constants';
|
||||||
import * as loc from './localizedConstants';
|
import * as loc from './localizedConstants';
|
||||||
|
import { ArcControllersOptionsSourceProvider } from './providers/arcControllersOptionsSourceProvider';
|
||||||
import { ConnectToControllerDialog } from './ui/dialogs/connectControllerDialog';
|
import { ConnectToControllerDialog } from './ui/dialogs/connectControllerDialog';
|
||||||
import { AzureArcTreeDataProvider } from './ui/tree/azureArcTreeDataProvider';
|
import { AzureArcTreeDataProvider } from './ui/tree/azureArcTreeDataProvider';
|
||||||
import { ControllerTreeNode } from './ui/tree/controllerTreeNode';
|
import { ControllerTreeNode } from './ui/tree/controllerTreeNode';
|
||||||
@@ -25,6 +28,14 @@ export async function activate(context: vscode.ExtensionContext): Promise<arc.IE
|
|||||||
});
|
});
|
||||||
|
|
||||||
vscode.commands.registerCommand('arc.connectToController', async () => {
|
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);
|
const dialog = new ConnectToControllerDialog(treeDataProvider);
|
||||||
dialog.showDialog();
|
dialog.showDialog();
|
||||||
const model = await dialog.waitForClose();
|
const model = await dialog.waitForClose();
|
||||||
@@ -54,28 +65,12 @@ export async function activate(context: vscode.ExtensionContext): Promise<arc.IE
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await checkArcDeploymentExtension();
|
// register option sources
|
||||||
|
const rdApi = <rd.IExtension>vscode.extensions.getExtension(rd.extension.name)?.exports;
|
||||||
|
rdApi.registerOptionsSourceProvider(new ArcControllersOptionsSourceProvider(treeDataProvider));
|
||||||
|
|
||||||
return {
|
return arcApi(treeDataProvider);
|
||||||
getRegisteredDataControllers: async () => {
|
|
||||||
return (await treeDataProvider.getChildren())
|
|
||||||
.filter(node => node instanceof ControllerTreeNode)
|
|
||||||
.map(node => (node as ControllerTreeNode).model.info);
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deactivate(): void {
|
export function deactivate(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkArcDeploymentExtension(): Promise<void> {
|
|
||||||
const version = vscode.extensions.getExtension('Microsoft.arcdeployment')?.packageJSON.version;
|
|
||||||
if (version && version !== '0.3.2') {
|
|
||||||
// If we have an older version of the deployment extension installed then uninstall it now since it's replaced
|
|
||||||
// by this extension. (the latest version of the Arc Deployment extension will uninstall itself so don't do
|
|
||||||
// anything here if that's already updated)
|
|
||||||
await vscode.commands.executeCommand('workbench.extensions.uninstallExtension', 'Microsoft.arcdeployment');
|
|
||||||
vscode.window.showInformationMessage(loc.arcDeploymentDeprecation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ import { getErrorMessage } from './common/utils';
|
|||||||
const localize = nls.loadMessageBundle();
|
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 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 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', "Managed Instance 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', "Postgres 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 dataControllersType = localize('arc.dataControllersType', "Azure Arc Data Controller");
|
||||||
export const pgSqlType = localize('arc.pgSqlType', "PostgreSQL Server group - Azure Arc");
|
export const pgSqlType = localize('arc.pgSqlType', "PostgreSQL Hyperscale - Azure Arc");
|
||||||
export const miaaType = localize('arc.miaaType', "SQL instance - Azure Arc");
|
export const miaaType = localize('arc.miaaType', "SQL managed instance - Azure Arc");
|
||||||
|
|
||||||
export const overview = localize('arc.overview', "Overview");
|
export const overview = localize('arc.overview', "Overview");
|
||||||
export const connectionStrings = localize('arc.connectionStrings', "Connection Strings");
|
export const connectionStrings = localize('arc.connectionStrings', "Connection Strings");
|
||||||
@@ -72,8 +72,12 @@ export const direct = localize('arc.direct', "Direct");
|
|||||||
export const indirect = localize('arc.indirect', "Indirect");
|
export const indirect = localize('arc.indirect', "Indirect");
|
||||||
export const loading = localize('arc.loading', "Loading...");
|
export const loading = localize('arc.loading', "Loading...");
|
||||||
export const refreshToEnterCredentials = localize('arc.refreshToEnterCredentials', "Refresh node to enter credentials");
|
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 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 controllerUrl = localize('arc.controllerUrl', "Controller URL");
|
||||||
|
export const serverEndpoint = localize('arc.serverEndpoint', "Server Endpoint");
|
||||||
export const controllerName = localize('arc.controllerName', "Name");
|
export const controllerName = localize('arc.controllerName', "Name");
|
||||||
export const defaultControllerName = localize('arc.defaultControllerName', "arc-dc");
|
export const defaultControllerName = localize('arc.defaultControllerName', "arc-dc");
|
||||||
export const username = localize('arc.username', "Username");
|
export const username = localize('arc.username', "Username");
|
||||||
@@ -81,6 +85,7 @@ export const password = localize('arc.password', "Password");
|
|||||||
export const rememberPassword = localize('arc.rememberPassword', "Remember Password");
|
export const rememberPassword = localize('arc.rememberPassword', "Remember Password");
|
||||||
export const connect = localize('arc.connect', "Connect");
|
export const connect = localize('arc.connect', "Connect");
|
||||||
export const cancel = localize('arc.cancel', "Cancel");
|
export const cancel = localize('arc.cancel', "Cancel");
|
||||||
|
export const ok = localize('arc.ok', "Ok");
|
||||||
export const notConfigured = localize('arc.notConfigured', "Not Configured");
|
export const notConfigured = localize('arc.notConfigured', "Not Configured");
|
||||||
|
|
||||||
// Database States - see https://docs.microsoft.com/sql/relational-databases/databases/database-states
|
// Database States - see https://docs.microsoft.com/sql/relational-databases/databases/database-states
|
||||||
@@ -121,9 +126,11 @@ export const condition = localize('arc.condition', "Condition");
|
|||||||
export const details = localize('arc.details', "Details");
|
export const details = localize('arc.details', "Details");
|
||||||
export const lastUpdated = localize('arc.lastUpdated', "Last updated");
|
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 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 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 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 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 {
|
export function numVCores(vCores: string | undefined): string {
|
||||||
@@ -144,15 +151,25 @@ export const connectionRequired = localize('arc.connectionRequired', "A connecti
|
|||||||
export const couldNotFindControllerRegistration = localize('arc.couldNotFindControllerRegistration', "Could not find controller registration.");
|
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 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 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 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 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 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 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 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 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 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 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 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 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 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)); }
|
||||||
|
export const onlyOneControllerSupported = localize('arc.onlyOneControllerSupported', "Only one controller connection is currently supported at this time. Do you wish to remove the existing connection and add a new one?");
|
||||||
|
export const noControllersConnected = localize('noControllersConnected', "No Azure Arc controllers are currently connected. Please run the command: 'Connect to Existing Azure Arc Controller' and then try again");
|
||||||
|
export const variableValueFetchForUnsupportedVariable = (variableName: string) => localize('getVariableValue.unknownVariableName', "Attempt to get variable value for unknown variable:{0}", variableName);
|
||||||
|
export const isPasswordFetchForUnsupportedVariable = (variableName: string) => localize('getIsPassword.unknownVariableName', "Attempt to get isPassword for unknown variable:{0}", variableName);
|
||||||
|
export const noControllerInfoFound = (name: string) => localize('noControllerInfoFound', "Controller Info could not be found with name: {0}", name);
|
||||||
|
export const noPasswordFound = (controllerName: string) => localize('noPasswordFound', "Password could not be retrieved for controller: {0} and user did not provide a password. Please retry later.", controllerName);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import { ControllerInfo, ResourceType } from 'arc';
|
import { ControllerInfo, ResourceType } from 'arc';
|
||||||
import * as azdataExt from 'azdata-ext';
|
import * as azdataExt from 'azdata-ext';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { parseInstanceName, UserCancelledError } from '../common/utils';
|
import { UserCancelledError } from '../common/utils';
|
||||||
import * as loc from '../localizedConstants';
|
import * as loc from '../localizedConstants';
|
||||||
import { ConnectToControllerDialog } from '../ui/dialogs/connectControllerDialog';
|
import { ConnectToControllerDialog } from '../ui/dialogs/connectControllerDialog';
|
||||||
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
|
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
|
||||||
@@ -20,7 +20,6 @@ export type Registration = {
|
|||||||
export class ControllerModel {
|
export class ControllerModel {
|
||||||
private readonly _azdataApi: azdataExt.IExtension;
|
private readonly _azdataApi: azdataExt.IExtension;
|
||||||
private _endpoints: azdataExt.DcEndpointListResult[] = [];
|
private _endpoints: azdataExt.DcEndpointListResult[] = [];
|
||||||
private _namespace: string = '';
|
|
||||||
private _registrations: Registration[] = [];
|
private _registrations: Registration[] = [];
|
||||||
private _controllerConfig: azdataExt.DcConfigShowResult | undefined = undefined;
|
private _controllerConfig: azdataExt.DcConfigShowResult | undefined = undefined;
|
||||||
|
|
||||||
@@ -93,7 +92,7 @@ export class ControllerModel {
|
|||||||
}
|
}
|
||||||
public async refresh(showErrors: boolean = true, promptReconnect: boolean = false): Promise<void> {
|
public async refresh(showErrors: boolean = true, promptReconnect: boolean = false): Promise<void> {
|
||||||
await this.azdataLogin(promptReconnect);
|
await this.azdataLogin(promptReconnect);
|
||||||
this._registrations = [];
|
const newRegistrations: Registration[] = [];
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this._azdataApi.azdata.arc.dc.config.show().then(result => {
|
this._azdataApi.azdata.arc.dc.config.show().then(result => {
|
||||||
this._controllerConfig = result.result;
|
this._controllerConfig = result.result;
|
||||||
@@ -125,7 +124,7 @@ export class ControllerModel {
|
|||||||
}),
|
}),
|
||||||
Promise.all([
|
Promise.all([
|
||||||
this._azdataApi.azdata.arc.postgres.server.list().then(result => {
|
this._azdataApi.azdata.arc.postgres.server.list().then(result => {
|
||||||
this._registrations.push(...result.result.map(r => {
|
newRegistrations.push(...result.result.map(r => {
|
||||||
return {
|
return {
|
||||||
instanceName: r.name,
|
instanceName: r.name,
|
||||||
state: r.state,
|
state: r.state,
|
||||||
@@ -134,7 +133,7 @@ export class ControllerModel {
|
|||||||
}));
|
}));
|
||||||
}),
|
}),
|
||||||
this._azdataApi.azdata.arc.sql.mi.list().then(result => {
|
this._azdataApi.azdata.arc.sql.mi.list().then(result => {
|
||||||
this._registrations.push(...result.result.map(r => {
|
newRegistrations.push(...result.result.map(r => {
|
||||||
return {
|
return {
|
||||||
instanceName: r.name,
|
instanceName: r.name,
|
||||||
state: r.state,
|
state: r.state,
|
||||||
@@ -143,6 +142,7 @@ export class ControllerModel {
|
|||||||
}));
|
}));
|
||||||
})
|
})
|
||||||
]).then(() => {
|
]).then(() => {
|
||||||
|
this._registrations = newRegistrations;
|
||||||
this.registrationsLastUpdated = new Date();
|
this.registrationsLastUpdated = new Date();
|
||||||
this._onRegistrationsUpdated.fire(this._registrations);
|
this._onRegistrationsUpdated.fire(this._registrations);
|
||||||
})
|
})
|
||||||
@@ -157,10 +157,6 @@ export class ControllerModel {
|
|||||||
return this._endpoints.find(e => e.name === name);
|
return this._endpoints.find(e => e.name === name);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get namespace(): string {
|
|
||||||
return this._namespace;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get registrations(): Registration[] {
|
public get registrations(): Registration[] {
|
||||||
return this._registrations;
|
return this._registrations;
|
||||||
}
|
}
|
||||||
@@ -171,19 +167,10 @@ export class ControllerModel {
|
|||||||
|
|
||||||
public getRegistration(type: ResourceType, name: string): Registration | undefined {
|
public getRegistration(type: ResourceType, name: string): Registration | undefined {
|
||||||
return this._registrations.find(r => {
|
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
|
* 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.
|
* 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 azdata from 'azdata';
|
||||||
import * as azdataExt from 'azdata-ext';
|
import * as azdataExt from 'azdata-ext';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { Deferred } from '../common/promise';
|
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 * as loc from '../localizedConstants';
|
||||||
|
import { ConnectToSqlDialog } from '../ui/dialogs/connectSqlDialog';
|
||||||
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
|
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
|
||||||
import { ControllerModel, Registration } from './controllerModel';
|
import { ControllerModel, Registration } from './controllerModel';
|
||||||
import { ResourceModel } from './resourceModel';
|
import { ResourceModel } from './resourceModel';
|
||||||
@@ -35,8 +37,8 @@ export class MiaaModel extends ResourceModel {
|
|||||||
|
|
||||||
private _refreshPromise: Deferred<void> | undefined = undefined;
|
private _refreshPromise: Deferred<void> | undefined = undefined;
|
||||||
|
|
||||||
constructor(private _controllerModel: ControllerModel, info: ResourceInfo, registration: Registration, private _treeDataProvider: AzureArcTreeDataProvider) {
|
constructor(private _controllerModel: ControllerModel, private _miaaInfo: MiaaResourceInfo, registration: Registration, private _treeDataProvider: AzureArcTreeDataProvider) {
|
||||||
super(info, registration);
|
super(_miaaInfo, registration);
|
||||||
this._azdataApi = <azdataExt.IExtension>vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
|
this._azdataApi = <azdataExt.IExtension>vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,83 +157,58 @@ export class MiaaModel extends ResourceModel {
|
|||||||
if (this._connectionProfile) {
|
if (this._connectionProfile) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let connection: azdata.connection.ConnectionProfile | azdata.connection.Connection | undefined;
|
|
||||||
|
|
||||||
|
const ipAndPort = parseIpAndPort(this.config?.status.externalEndpoint || '');
|
||||||
|
let connectionProfile: azdata.IConnectionProfile | undefined = {
|
||||||
|
serverName: `${ipAndPort.ip},${ipAndPort.port}`,
|
||||||
|
databaseName: '',
|
||||||
|
authenticationType: 'SqlLogin',
|
||||||
|
providerName: 'MSSQL',
|
||||||
|
connectionName: '',
|
||||||
|
userName: this._miaaInfo.userName || '',
|
||||||
|
password: '',
|
||||||
|
savePassword: true,
|
||||||
|
groupFullName: undefined,
|
||||||
|
saveProfile: true,
|
||||||
|
id: '',
|
||||||
|
groupId: undefined,
|
||||||
|
options: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// If we have the ID stored then try to retrieve the password from previous connections
|
||||||
if (this.info.connectionId) {
|
if (this.info.connectionId) {
|
||||||
try {
|
try {
|
||||||
const connections = await azdata.connection.getConnections();
|
const credentialProvider = await azdata.credentials.getProvider(credentialNamespace);
|
||||||
const existingConnection = connections.find(conn => conn.connectionId === this.info.connectionId);
|
const credentials = await credentialProvider.readCredential(createCredentialId(this._controllerModel.info.id, this.info.resourceType, this.info.name));
|
||||||
if (existingConnection) {
|
if (credentials.password) {
|
||||||
const credentials = await azdata.connection.getCredentials(this.info.connectionId);
|
// Try to connect to verify credentials are still valid
|
||||||
if (credentials) {
|
connectionProfile.password = credentials.password;
|
||||||
existingConnection.options['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
|
||||||
connection = existingConnection;
|
if (connectionProfile.userName) {
|
||||||
} else {
|
const result = await azdata.connection.connect(connectionProfile, false, false);
|
||||||
// We need the password so prompt the user for it
|
if (!result.connected) {
|
||||||
const connectionProfile: azdata.IConnectionProfile = {
|
vscode.window.showErrorMessage(loc.connectToSqlFailed(connectionProfile.serverName, result.errorMessage));
|
||||||
serverName: existingConnection.options['serverName'],
|
const connectToSqlDialog = new ConnectToSqlDialog(this._controllerModel, this);
|
||||||
databaseName: existingConnection.options['databaseName'],
|
connectToSqlDialog.showDialog(connectionProfile);
|
||||||
authenticationType: existingConnection.options['authenticationType'],
|
connectionProfile = await connectToSqlDialog.waitForClose();
|
||||||
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) {
|
} catch (err) {
|
||||||
// ignore - the connection may not necessarily exist anymore and in that case we'll just reprompt for a connection
|
console.warn(`Unexpected error fetching password for MIAA instance ${err}`);
|
||||||
|
// ignore - something happened fetching the password so just reprompt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!connection) {
|
if (!connectionProfile?.userName || !connectionProfile?.password) {
|
||||||
// We need the password so prompt the user for it
|
// Need to prompt user for password since we don't have one stored
|
||||||
const connectionProfile: azdata.IConnectionProfile = {
|
const connectToSqlDialog = new ConnectToSqlDialog(this._controllerModel, this);
|
||||||
// TODO chgagnon fill in external IP and port
|
connectToSqlDialog.showDialog(connectionProfile);
|
||||||
// serverName: (this.registration.externalIp && this.registration.externalPort) ? `${this.registration.externalIp},${this.registration.externalPort}` : '',
|
connectionProfile = await connectToSqlDialog.waitForClose();
|
||||||
serverName: '',
|
|
||||||
databaseName: '',
|
|
||||||
authenticationType: 'SqlLogin',
|
|
||||||
providerName: 'MSSQL',
|
|
||||||
connectionName: '',
|
|
||||||
userName: 'sa',
|
|
||||||
password: '',
|
|
||||||
savePassword: true,
|
|
||||||
groupFullName: undefined,
|
|
||||||
saveProfile: true,
|
|
||||||
id: '',
|
|
||||||
groupId: undefined,
|
|
||||||
options: {}
|
|
||||||
};
|
|
||||||
// Weren't able to load the existing connection so prompt user for new one
|
|
||||||
connection = await azdata.connection.openConnectionDialog(['MSSQL'], connectionProfile);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connection) {
|
if (connectionProfile) {
|
||||||
const profile = {
|
this.updateConnectionProfile(connectionProfile);
|
||||||
// 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);
|
|
||||||
} else {
|
} else {
|
||||||
throw new UserCancelledError();
|
throw new UserCancelledError();
|
||||||
}
|
}
|
||||||
@@ -240,6 +217,7 @@ export class MiaaModel extends ResourceModel {
|
|||||||
private async updateConnectionProfile(connectionProfile: azdata.IConnectionProfile): Promise<void> {
|
private async updateConnectionProfile(connectionProfile: azdata.IConnectionProfile): Promise<void> {
|
||||||
this._connectionProfile = connectionProfile;
|
this._connectionProfile = connectionProfile;
|
||||||
this.info.connectionId = connectionProfile.id;
|
this.info.connectionId = connectionProfile.id;
|
||||||
|
this._miaaInfo.userName = connectionProfile.userName;
|
||||||
await this._treeDataProvider.saveControllers();
|
await this._treeDataProvider.saveControllers();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,278 +4,83 @@
|
|||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import { ResourceInfo } from 'arc';
|
import { ResourceInfo } from 'arc';
|
||||||
|
import * as azdataExt from 'azdata-ext';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import * as loc from '../localizedConstants';
|
import * as loc from '../localizedConstants';
|
||||||
import { Registration } from './controllerModel';
|
import { ControllerModel, Registration } from './controllerModel';
|
||||||
import { ResourceModel } from './resourceModel';
|
import { ResourceModel } from './resourceModel';
|
||||||
|
import { parseIpAndPort } from '../common/utils';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PostgresModel extends ResourceModel {
|
export class PostgresModel extends ResourceModel {
|
||||||
private _service?: DuskyObjectModelsDatabaseService;
|
private _config?: azdataExt.PostgresServerShowResult;
|
||||||
private _pods?: V1Pod[];
|
private readonly _azdataApi: azdataExt.IExtension;
|
||||||
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;
|
|
||||||
|
|
||||||
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);
|
super(info, registration);
|
||||||
|
this._azdataApi = <azdataExt.IExtension>vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the service's Kubernetes namespace */
|
/** Returns the configuration of Postgres */
|
||||||
public get namespace(): string | undefined {
|
public get config(): azdataExt.PostgresServerShowResult | undefined {
|
||||||
return ''; // TODO chgagnon return this.info.namespace;
|
return this._config;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the service's name */
|
/** Returns the major version of Postgres */
|
||||||
public get name(): string {
|
public get engineVersion(): string | undefined {
|
||||||
return this.info.name;
|
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 */
|
/** Returns the IP address and port of Postgres */
|
||||||
public get fullName(): string {
|
public get endpoint(): { ip: string, port: string } | undefined {
|
||||||
return `${this.namespace}.${this.name}`;
|
return this._config?.status.externalEndpoint
|
||||||
|
? parseIpAndPort(this._config.status.externalEndpoint)
|
||||||
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the service's spec */
|
/** Returns the scale configuration of Postgres e.g. '3 nodes, 1.5 vCores, 1Gi RAM, 2Gi storage per node' */
|
||||||
public get service(): DuskyObjectModelsDatabaseService | undefined {
|
public get scaleConfiguration(): string | undefined {
|
||||||
return this._service;
|
if (!this._config) {
|
||||||
}
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns the service's pods */
|
const cpuLimit = this._config.spec.scheduling?.default?.resources?.limits?.cpu;
|
||||||
public get pods(): V1Pod[] | undefined {
|
const ramLimit = this._config.spec.scheduling?.default?.resources?.limits?.memory;
|
||||||
return this._pods;
|
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;
|
||||||
/** Refreshes the model */
|
const nodes = (this._config.spec.scale?.shards ?? 0) + 1; // An extra node for the coordinator
|
||||||
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;
|
|
||||||
|
|
||||||
let configuration: string[] = [];
|
let configuration: string[] = [];
|
||||||
|
configuration.push(`${nodes} ${nodes > 1 ? loc.nodes : loc.node}`);
|
||||||
if (nodes) {
|
|
||||||
configuration.push(`${nodes} ${nodes > 1 ? loc.nodes : loc.node}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prefer limits if they're provided, otherwise use requests if they're provided
|
// Prefer limits if they're provided, otherwise use requests if they're provided
|
||||||
if (cpuLimit || cpuRequest) {
|
if (cpuLimit || cpuRequest) {
|
||||||
configuration.push(`${this.formatCores(cpuLimit ?? cpuRequest!)} ${loc.vCores}`);
|
configuration.push(`${cpuLimit ?? cpuRequest!} ${loc.vCores}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ramLimit || ramRequest) {
|
if (ramLimit || ramRequest) {
|
||||||
configuration.push(`${this.formatMemory(ramLimit ?? ramRequest!)} ${loc.ram}`);
|
configuration.push(`${ramLimit ?? ramRequest!} ${loc.ram}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (storage) {
|
if (storage) {
|
||||||
configuration.push(`${this.formatMemory(storage)} ${loc.storagePerNode}`);
|
configuration.push(`${storage} ${loc.storagePerNode}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return configuration.join(', ');
|
return configuration.join(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Given a V1Pod, returns its PodRole or undefined if the role isn't known */
|
/** Refreshes the model */
|
||||||
public static getPodRole(pod: V1Pod): PodRole | undefined {
|
public async refresh() {
|
||||||
const name = pod.metadata?.name;
|
await this._controllerModel.azdataLogin();
|
||||||
const role = name?.substring(name.lastIndexOf('-'))[1];
|
this._config = (await this._azdataApi.azdata.arc.postgres.server.show(this.info.name)).result;
|
||||||
switch (role) {
|
this.configLastUpdated = new Date();
|
||||||
case 'm': return PodRole.Monitor;
|
this._onConfigUpdated.fire(this._config);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import * as arc from 'arc';
|
||||||
|
import * as azdata from 'azdata';
|
||||||
|
import * as rd from 'resource-deployment';
|
||||||
|
import { getControllerPassword, getRegisteredDataControllers, reacquireControllerPassword } from '../common/api';
|
||||||
|
import { CacheManager } from '../common/cacheManager';
|
||||||
|
import { throwUnless } from '../common/utils';
|
||||||
|
import * as loc from '../localizedConstants';
|
||||||
|
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class that provides options sources for an Arc Data Controller
|
||||||
|
*/
|
||||||
|
export class ArcControllersOptionsSourceProvider implements rd.IOptionsSourceProvider {
|
||||||
|
private _cacheManager = new CacheManager<string, string>();
|
||||||
|
readonly optionsSourceId = 'arc.controllers';
|
||||||
|
constructor(private _treeProvider: AzureArcTreeDataProvider) { }
|
||||||
|
|
||||||
|
async getOptions(): Promise<string[] | azdata.CategoryValue[]> {
|
||||||
|
const controllers = await getRegisteredDataControllers(this._treeProvider);
|
||||||
|
throwUnless(controllers !== undefined && controllers.length !== 0, loc.noControllersConnected);
|
||||||
|
return controllers.map(ci => {
|
||||||
|
return ci.label;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async retrieveVariable(key: string): Promise<string> {
|
||||||
|
const [variableName, controllerLabel] = JSON.parse(key);
|
||||||
|
const controller = (await getRegisteredDataControllers(this._treeProvider)).find(ci => ci.label === controllerLabel);
|
||||||
|
throwUnless(controller !== undefined, loc.noControllerInfoFound(controllerLabel));
|
||||||
|
switch (variableName) {
|
||||||
|
case 'endpoint': return controller.info.url;
|
||||||
|
case 'username': return controller.info.username;
|
||||||
|
case 'password': return this.getPassword(controller);
|
||||||
|
default: throw new Error(loc.variableValueFetchForUnsupportedVariable(variableName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getVariableValue(variableName: string, controllerLabel: string): Promise<string> {
|
||||||
|
// 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> {
|
||||||
|
let password = await getControllerPassword(this._treeProvider, controller.info);
|
||||||
|
if (!password) {
|
||||||
|
password = await reacquireControllerPassword(this._treeProvider, controller.info);
|
||||||
|
}
|
||||||
|
throwUnless(password !== undefined, loc.noPasswordFound(controller.label));
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
getIsPassword(variableName: string): boolean {
|
||||||
|
switch (variableName) {
|
||||||
|
case 'endpoint': return false;
|
||||||
|
case 'username': return false;
|
||||||
|
case 'password': return true;
|
||||||
|
default: throw new Error(loc.isPasswordFetchForUnsupportedVariable(variableName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import { ResourceType } from 'arc';
|
|||||||
import 'mocha';
|
import 'mocha';
|
||||||
import * as should from 'should';
|
import * as should from 'should';
|
||||||
import * as vscode from 'vscode';
|
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 { ConnectionMode as ConnectionMode, IconPathHelper } from '../../constants';
|
||||||
import * as loc from '../../localizedConstants';
|
import * as loc from '../../localizedConstants';
|
||||||
import { MockInputBox } from '../stubs';
|
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 () {
|
describe('getAzurecoreApi Method Tests', function () {
|
||||||
it('Should get azurecore API correctly', function (): void {
|
it('Should get azurecore API correctly', function (): void {
|
||||||
should(getAzurecoreApi()).not.be.undefined();
|
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 {
|
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'));
|
value ? done() : done(new Error('Expected return value to be true'));
|
||||||
});
|
});
|
||||||
mockInputBox.value = 'myname';
|
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 {
|
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'));
|
!value ? done() : done(new Error('Expected return value to be false'));
|
||||||
});
|
});
|
||||||
mockInputBox.hide();
|
mockInputBox.hide();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Validation message is set when value entered is incorrect', async function (): Promise<void> {
|
it('Validation message is set when value entered is incorrect', async function (): Promise<void> {
|
||||||
promptForResourceDeletion('myname');
|
promptForInstanceDeletion('myname');
|
||||||
mockInputBox.value = 'wrong value';
|
mockInputBox.value = 'wrong value';
|
||||||
await mockInputBox.triggerAccept();
|
await mockInputBox.triggerAccept();
|
||||||
should(mockInputBox.validationMessage).not.be.equal('', 'Validation message should not be empty after incorrect value entered');
|
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 {
|
describe('parseIpAndPort', function (): void {
|
||||||
it('Valid address', function (): void {
|
it('Valid address', function (): void {
|
||||||
const ip = '127.0.0.1';
|
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.
|
* 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 'mocha';
|
||||||
import * as should from 'should';
|
import * as should from 'should';
|
||||||
import * as TypeMoq from 'typemoq';
|
import * as TypeMoq from 'typemoq';
|
||||||
|
import * as sinon from 'sinon';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
|
import * as azdataExt from 'azdata-ext';
|
||||||
import { ControllerModel } from '../../../models/controllerModel';
|
import { ControllerModel } from '../../../models/controllerModel';
|
||||||
|
import { MiaaModel } from '../../../models/miaaModel';
|
||||||
import { AzureArcTreeDataProvider } from '../../../ui/tree/azureArcTreeDataProvider';
|
import { AzureArcTreeDataProvider } from '../../../ui/tree/azureArcTreeDataProvider';
|
||||||
import { ControllerTreeNode } from '../../../ui/tree/controllerTreeNode';
|
import { ControllerTreeNode } from '../../../ui/tree/controllerTreeNode';
|
||||||
|
import { MiaaTreeNode } from '../../../ui/tree/miaaTreeNode';
|
||||||
import { FakeControllerModel } from '../../mocks/fakeControllerModel';
|
import { FakeControllerModel } from '../../mocks/fakeControllerModel';
|
||||||
|
import { FakeAzdataApi } from '../../mocks/fakeAzdataApi';
|
||||||
|
|
||||||
describe('AzureArcTreeDataProvider tests', function (): void {
|
describe('AzureArcTreeDataProvider tests', function (): void {
|
||||||
let treeDataProvider: AzureArcTreeDataProvider;
|
let treeDataProvider: AzureArcTreeDataProvider;
|
||||||
@@ -84,6 +89,27 @@ describe('AzureArcTreeDataProvider tests', function (): void {
|
|||||||
let children = await treeDataProvider.getChildren();
|
let children = await treeDataProvider.getChildren();
|
||||||
should(children.length).equal(0, 'After loading we should have 0 children');
|
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 {
|
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');
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
13
extensions/arc/src/typings/arc.d.ts
vendored
13
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.
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
declare module 'arc' {
|
declare module 'arc' {
|
||||||
import * as vscode from 'vscode';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Covers defining what the arc extension exports to other extensions
|
* Covers defining what the arc extension exports to other extensions
|
||||||
@@ -20,6 +19,10 @@ declare module 'arc' {
|
|||||||
sqlManagedInstances = 'sqlManagedInstances'
|
sqlManagedInstances = 'sqlManagedInstances'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MiaaResourceInfo = ResourceInfo & {
|
||||||
|
userName?: string
|
||||||
|
};
|
||||||
|
|
||||||
export type ResourceInfo = {
|
export type ResourceInfo = {
|
||||||
name: string,
|
name: string,
|
||||||
resourceType: ResourceType | string,
|
resourceType: ResourceType | string,
|
||||||
@@ -35,7 +38,13 @@ declare module 'arc' {
|
|||||||
resources: ResourceInfo[]
|
resources: ResourceInfo[]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface DataController {
|
||||||
|
label: string,
|
||||||
|
info: ControllerInfo
|
||||||
|
}
|
||||||
export interface IExtension {
|
export interface IExtension {
|
||||||
getRegisteredDataControllers(): Promise<ControllerInfo[]>;
|
getRegisteredDataControllers(): Promise<DataController[]>;
|
||||||
|
getControllerPassword(controllerInfo: ControllerInfo): Promise<string>;
|
||||||
|
reacquireControllerPassword(controllerInfo: ControllerInfo, password: string, retryCount?: number): Promise<string>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
extensions/arc/src/typings/refs.d.ts
vendored
1
extensions/arc/src/typings/refs.d.ts
vendored
@@ -8,3 +8,4 @@
|
|||||||
/// <reference path='../../../azurecore/src/azurecore.d.ts'/>
|
/// <reference path='../../../azurecore/src/azurecore.d.ts'/>
|
||||||
/// <reference path='../../../../src/vs/vscode.d.ts'/>
|
/// <reference path='../../../../src/vs/vscode.d.ts'/>
|
||||||
/// <reference path='../../../azdata/src/typings/azdata-ext.d.ts'/>
|
/// <reference path='../../../azdata/src/typings/azdata-ext.d.ts'/>
|
||||||
|
/// <reference path='../../../resource-deployment/src/typings/resource-deployment.d.ts'/>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export abstract class Dashboard {
|
|||||||
|
|
||||||
private dashboard!: azdata.window.ModelViewDashboard;
|
private dashboard!: azdata.window.ModelViewDashboard;
|
||||||
|
|
||||||
constructor(protected title: string) { }
|
constructor(protected title: string, protected readonly name: string) { }
|
||||||
|
|
||||||
public async showDashboard(): Promise<void> {
|
public async showDashboard(): Promise<void> {
|
||||||
this.dashboard = this.createDashboard();
|
this.dashboard = this.createDashboard();
|
||||||
@@ -17,7 +17,7 @@ export abstract class Dashboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected createDashboard(): azdata.window.ModelViewDashboard {
|
protected createDashboard(): azdata.window.ModelViewDashboard {
|
||||||
const dashboard = azdata.window.createModelViewDashboard(this.title);
|
const dashboard = azdata.window.createModelViewDashboard(this.title, this.name);
|
||||||
dashboard.registerTabs(async modelView => {
|
dashboard.registerTabs(async modelView => {
|
||||||
return await this.registerTabs(modelView);
|
return await this.registerTabs(modelView);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import * as loc from '../../../localizedConstants';
|
|||||||
export class ControllerDashboard extends Dashboard {
|
export class ControllerDashboard extends Dashboard {
|
||||||
|
|
||||||
constructor(private _controllerModel: ControllerModel) {
|
constructor(private _controllerModel: ControllerModel) {
|
||||||
super(loc.arcControllerDashboard(_controllerModel.info.name));
|
super(loc.arcControllerDashboard(_controllerModel.info.name), 'ArcDataControllerDashboard');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async showDashboard(): Promise<void> {
|
public async showDashboard(): Promise<void> {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import { ResourceType } from 'arc';
|
|||||||
import * as azdata from 'azdata';
|
import * as azdata from 'azdata';
|
||||||
import * as azurecore from 'azurecore';
|
import * as azurecore from 'azurecore';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { getConnectionModeDisplayText, getResourceTypeIcon, parseInstanceName, resourceTypeToDisplayName } from '../../../common/utils';
|
import { getConnectionModeDisplayText, getResourceTypeIcon, resourceTypeToDisplayName } from '../../../common/utils';
|
||||||
import { cssStyles, Endpoints, IconPathHelper, iconSize } from '../../../constants';
|
import { cssStyles, Endpoints, IconPathHelper, controllerTroubleshootDocsUrl, iconSize } from '../../../constants';
|
||||||
import * as loc from '../../../localizedConstants';
|
import * as loc from '../../../localizedConstants';
|
||||||
import { ControllerModel } from '../../../models/controllerModel';
|
import { ControllerModel } from '../../../models/controllerModel';
|
||||||
import { DashboardPage } from '../../components/dashboardPage';
|
import { DashboardPage } from '../../components/dashboardPage';
|
||||||
@@ -93,7 +93,7 @@ export class ControllerDashboardOverviewPage extends DashboardPage {
|
|||||||
headerCssStyles: cssStyles.tableHeader,
|
headerCssStyles: cssStyles.tableHeader,
|
||||||
rowCssStyles: cssStyles.tableRow
|
rowCssStyles: cssStyles.tableRow
|
||||||
}, {
|
}, {
|
||||||
displayName: loc.compute,
|
displayName: loc.state,
|
||||||
valueType: azdata.DeclarativeDataType.string,
|
valueType: azdata.DeclarativeDataType.string,
|
||||||
width: '34%',
|
width: '34%',
|
||||||
isReadOnly: true,
|
isReadOnly: true,
|
||||||
@@ -178,18 +178,30 @@ export class ControllerDashboardOverviewPage extends DashboardPage {
|
|||||||
this._openInAzurePortalButton.onDidClick(async () => {
|
this._openInAzurePortalButton.onDidClick(async () => {
|
||||||
const config = this._controllerModel.controllerConfig;
|
const config = this._controllerModel.controllerConfig;
|
||||||
if (config) {
|
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}`));
|
`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 {
|
} else {
|
||||||
vscode.window.showErrorMessage(loc.couldNotFindControllerRegistration);
|
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(
|
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems(
|
||||||
[
|
[
|
||||||
{ component: newInstance },
|
{ component: newInstance },
|
||||||
{ component: refreshButton, toolbarSeparatorAfter: true },
|
{ component: refreshButton, toolbarSeparatorAfter: true },
|
||||||
{ component: this._openInAzurePortalButton }
|
{ component: this._openInAzurePortalButton, toolbarSeparatorAfter: true },
|
||||||
|
{ component: troubleshootButton }
|
||||||
]
|
]
|
||||||
).component();
|
).component();
|
||||||
}
|
}
|
||||||
@@ -219,26 +231,18 @@ export class ControllerDashboardOverviewPage extends DashboardPage {
|
|||||||
iconHeight: iconSize,
|
iconHeight: iconSize,
|
||||||
iconWidth: iconSize
|
iconWidth: iconSize
|
||||||
}).component();
|
}).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()
|
|
||||||
.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
|
const nameComponent = this.modelView.modelBuilder.hyperlink()
|
||||||
return [imageComponent, nameComponent, resourceTypeToDisplayName(r.instanceType), '-'/* loc.numVCores(r.vCores) */];
|
.withProperties<azdata.HyperlinkComponentProperties>({
|
||||||
|
label: r.instanceName || '',
|
||||||
|
url: ''
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
this.disposables.push(nameComponent.onDidClick(async () => {
|
||||||
|
await this._controllerModel.treeDataProvider.openResourceDashboard(this._controllerModel, r.instanceType || '', r.instanceName);
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [imageComponent, nameComponent, resourceTypeToDisplayName(r.instanceType), r.state];
|
||||||
});
|
});
|
||||||
this._arcResourcesLoadingComponent.loading = !this._controllerModel.registrationsLastUpdated;
|
this._arcResourcesLoadingComponent.loading = !this._controllerModel.registrationsLastUpdated;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { MiaaModel } from '../../../models/miaaModel';
|
|||||||
export class MiaaDashboard extends Dashboard {
|
export class MiaaDashboard extends Dashboard {
|
||||||
|
|
||||||
constructor(private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) {
|
constructor(private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) {
|
||||||
super(loc.miaaDashboard(_miaaModel.info.name));
|
super(loc.miaaDashboard(_miaaModel.info.name), 'ArcMiaaDashboard');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async showDashboard(): Promise<void> {
|
public async showDashboard(): Promise<void> {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import * as azdata from 'azdata';
|
|||||||
import * as azdataExt from 'azdata-ext';
|
import * as azdataExt from 'azdata-ext';
|
||||||
import * as azurecore from 'azurecore';
|
import * as azurecore from 'azurecore';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { getDatabaseStateDisplayText, promptForResourceDeletion } from '../../../common/utils';
|
import { getDatabaseStateDisplayText, promptForInstanceDeletion } from '../../../common/utils';
|
||||||
import { cssStyles, Endpoints, IconPathHelper } from '../../../constants';
|
import { cssStyles, IconPathHelper, miaaTroubleshootDocsUrl } from '../../../constants';
|
||||||
import * as loc from '../../../localizedConstants';
|
import * as loc from '../../../localizedConstants';
|
||||||
import { ControllerModel } from '../../../models/controllerModel';
|
import { ControllerModel } from '../../../models/controllerModel';
|
||||||
import { MiaaModel } from '../../../models/miaaModel';
|
import { MiaaModel } from '../../../models/miaaModel';
|
||||||
@@ -198,13 +198,22 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
|
|||||||
deleteButton.onDidClick(async () => {
|
deleteButton.onDidClick(async () => {
|
||||||
deleteButton.enabled = false;
|
deleteButton.enabled = false;
|
||||||
try {
|
try {
|
||||||
if (await promptForResourceDeletion(this._miaaModel.info.name)) {
|
if (await promptForInstanceDeletion(this._miaaModel.info.name)) {
|
||||||
await this._azdataApi.azdata.arc.sql.mi.delete(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();
|
await this._controllerModel.refreshTreeNode();
|
||||||
vscode.window.showInformationMessage(loc.resourceDeleted(this._miaaModel.info.name));
|
vscode.window.showInformationMessage(loc.instanceDeleted(this._miaaModel.info.name));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
vscode.window.showErrorMessage(loc.resourceDeletionFailed(this._miaaModel.info.name, error));
|
vscode.window.showErrorMessage(loc.instanceDeletionFailed(this._miaaModel.info.name, error));
|
||||||
} finally {
|
} finally {
|
||||||
deleteButton.enabled = true;
|
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(
|
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems(
|
||||||
[
|
[
|
||||||
{ component: deleteButton },
|
{ component: deleteButton },
|
||||||
@@ -332,19 +352,13 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private refreshDashboardLinks(): void {
|
private refreshDashboardLinks(): void {
|
||||||
const kibanaEndpoint = this._controllerModel.getEndpoint(Endpoints.logsui);
|
if (this._miaaModel.config) {
|
||||||
if (kibanaEndpoint && this._miaaModel.config) {
|
const kibanaUrl = this._miaaModel.config.status.logSearchDashboard ?? '';
|
||||||
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}'))`;
|
|
||||||
this._kibanaLink.label = kibanaUrl;
|
this._kibanaLink.label = kibanaUrl;
|
||||||
this._kibanaLink.url = kibanaUrl;
|
this._kibanaLink.url = kibanaUrl;
|
||||||
this._kibanaLoading!.loading = false;
|
this._kibanaLoading!.loading = false;
|
||||||
}
|
|
||||||
|
|
||||||
const grafanaEndpoint = this._controllerModel.getEndpoint(Endpoints.metricsui);
|
const grafanaUrl = this._miaaModel.config.status.metricsDashboard ?? '';
|
||||||
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}` : '';
|
|
||||||
this._grafanaLink.label = grafanaUrl;
|
this._grafanaLink.label = grafanaUrl;
|
||||||
this._grafanaLink.url = grafanaUrl;
|
this._grafanaLink.url = grafanaUrl;
|
||||||
this._grafanaLoading!.loading = false;
|
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.
|
* 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 azdata from 'azdata';
|
||||||
import * as loc from '../../../localizedConstants';
|
import * as loc from '../../../localizedConstants';
|
||||||
import { IconPathHelper, cssStyles } from '../../../constants';
|
import { IconPathHelper, cssStyles } from '../../../constants';
|
||||||
@@ -12,13 +11,12 @@ import { DashboardPage } from '../../components/dashboardPage';
|
|||||||
import { PostgresModel } from '../../../models/postgresModel';
|
import { PostgresModel } from '../../../models/postgresModel';
|
||||||
|
|
||||||
export class PostgresConnectionStringsPage extends DashboardPage {
|
export class PostgresConnectionStringsPage extends DashboardPage {
|
||||||
private loading?: azdata.LoadingComponent;
|
|
||||||
private keyValueContainer?: KeyValueContainer;
|
private keyValueContainer?: KeyValueContainer;
|
||||||
|
|
||||||
constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) {
|
constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) {
|
||||||
super(modelView);
|
super(modelView);
|
||||||
|
|
||||||
this.disposables.push(this._postgresModel.onServiceUpdated(
|
this.disposables.push(this._postgresModel.onConfigUpdated(
|
||||||
() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())));
|
() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,44 +59,20 @@ export class PostgresConnectionStringsPage extends DashboardPage {
|
|||||||
|
|
||||||
this.keyValueContainer = new KeyValueContainer(this.modelView.modelBuilder, this.getConnectionStrings());
|
this.keyValueContainer = new KeyValueContainer(this.modelView.modelBuilder, this.getConnectionStrings());
|
||||||
this.disposables.push(this.keyValueContainer);
|
this.disposables.push(this.keyValueContainer);
|
||||||
|
content.addItem(this.keyValueContainer.container);
|
||||||
this.loading = this.modelView.modelBuilder.loadingComponent()
|
|
||||||
.withItem(this.keyValueContainer.container)
|
|
||||||
.withProperties<azdata.LoadingComponentProperties>({
|
|
||||||
loading: !this._postgresModel.serviceLastUpdated
|
|
||||||
}).component();
|
|
||||||
|
|
||||||
content.addItem(this.loading);
|
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get toolbarContainer(): azdata.ToolbarContainer {
|
protected get toolbarContainer(): azdata.ToolbarContainer {
|
||||||
const refreshButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
|
return this.modelView.modelBuilder.toolbarContainer().component();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getConnectionStrings(): KeyValue[] {
|
private getConnectionStrings(): KeyValue[] {
|
||||||
const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint;
|
const endpoint = this._postgresModel.endpoint;
|
||||||
|
if (!endpoint) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
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;`),
|
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() {
|
private handleServiceUpdated() {
|
||||||
this.keyValueContainer?.refresh(this.getConnectionStrings());
|
this.keyValueContainer?.refresh(this.getConnectionStrings());
|
||||||
this.loading!.loading = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,15 +10,13 @@ import { ControllerModel } from '../../../models/controllerModel';
|
|||||||
import { PostgresModel } from '../../../models/postgresModel';
|
import { PostgresModel } from '../../../models/postgresModel';
|
||||||
import { PostgresOverviewPage } from './postgresOverviewPage';
|
import { PostgresOverviewPage } from './postgresOverviewPage';
|
||||||
import { PostgresConnectionStringsPage } from './postgresConnectionStringsPage';
|
import { PostgresConnectionStringsPage } from './postgresConnectionStringsPage';
|
||||||
import { PostgresPropertiesPage } from './postgresPropertiesPage';
|
|
||||||
import { Dashboard } from '../../components/dashboard';
|
import { Dashboard } from '../../components/dashboard';
|
||||||
import { PostgresDiagnoseAndSolveProblemsPage } from './postgresDiagnoseAndSolveProblemsPage';
|
import { PostgresDiagnoseAndSolveProblemsPage } from './postgresDiagnoseAndSolveProblemsPage';
|
||||||
import { PostgresSupportRequestPage } from './postgresSupportRequestPage';
|
import { PostgresSupportRequestPage } from './postgresSupportRequestPage';
|
||||||
import { PostgresResourceHealthPage } from './postgresResourceHealthPage';
|
|
||||||
|
|
||||||
export class PostgresDashboard extends Dashboard {
|
export class PostgresDashboard extends Dashboard {
|
||||||
constructor(private _context: vscode.ExtensionContext, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
|
constructor(private _context: vscode.ExtensionContext, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
|
||||||
super(loc.postgresDashboard(_postgresModel.name));
|
super(loc.postgresDashboard(_postgresModel.info.name), 'ArcPgDashboard');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async showDashboard(): Promise<void> {
|
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)[]> {
|
protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> {
|
||||||
const overviewPage = new PostgresOverviewPage(modelView, this._controllerModel, this._postgresModel);
|
const overviewPage = new PostgresOverviewPage(modelView, this._controllerModel, this._postgresModel);
|
||||||
const connectionStringsPage = new PostgresConnectionStringsPage(modelView, this._postgresModel);
|
const connectionStringsPage = new PostgresConnectionStringsPage(modelView, this._postgresModel);
|
||||||
const propertiesPage = new PostgresPropertiesPage(modelView, this._controllerModel, this._postgresModel);
|
// TODO: Removed properties page while investigating bug where refreshed values don't appear in UI
|
||||||
const resourceHealthPage = new PostgresResourceHealthPage(modelView, this._postgresModel);
|
// const propertiesPage = new PostgresPropertiesPage(modelView, this._controllerModel, this._postgresModel);
|
||||||
const diagnoseAndSolveProblemsPage = new PostgresDiagnoseAndSolveProblemsPage(modelView, this._context, 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 [
|
return [
|
||||||
overviewPage.tab,
|
overviewPage.tab,
|
||||||
{
|
{
|
||||||
title: loc.settings,
|
title: loc.settings,
|
||||||
tabs: [
|
tabs: [
|
||||||
connectionStringsPage.tab,
|
connectionStringsPage.tab
|
||||||
propertiesPage.tab
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: loc.supportAndTroubleshooting,
|
title: loc.supportAndTroubleshooting,
|
||||||
tabs: [
|
tabs: [
|
||||||
resourceHealthPage.tab,
|
|
||||||
diagnoseAndSolveProblemsPage.tab,
|
diagnoseAndSolveProblemsPage.tab,
|
||||||
supportRequestPage.tab
|
supportRequestPage.tab
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -50,8 +50,9 @@ export class PostgresDiagnoseAndSolveProblemsPage extends DashboardPage {
|
|||||||
|
|
||||||
this.disposables.push(
|
this.disposables.push(
|
||||||
troubleshootButton.onDidClick(() => {
|
troubleshootButton.onDidClick(() => {
|
||||||
process.env['POSTGRES_SERVER_NAMESPACE'] = this._postgresModel.namespace;
|
process.env['POSTGRES_SERVER_NAMESPACE'] = this._postgresModel.config?.metadata.namespace;
|
||||||
process.env['POSTGRES_SERVER_NAME'] = this._postgresModel.name;
|
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');
|
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 vscode from 'vscode';
|
||||||
import * as azdata from 'azdata';
|
import * as azdata from 'azdata';
|
||||||
|
import * as azdataExt from 'azdata-ext';
|
||||||
import * as loc from '../../../localizedConstants';
|
import * as loc from '../../../localizedConstants';
|
||||||
import { IconPathHelper, cssStyles, Endpoints } from '../../../constants';
|
import { IconPathHelper, cssStyles } from '../../../constants';
|
||||||
import { DashboardPage } from '../../components/dashboardPage';
|
import { DashboardPage } from '../../components/dashboardPage';
|
||||||
import { ControllerModel } from '../../../models/controllerModel';
|
import { ControllerModel } from '../../../models/controllerModel';
|
||||||
import { PostgresModel } from '../../../models/postgresModel';
|
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 {
|
export class PostgresOverviewPage extends DashboardPage {
|
||||||
|
|
||||||
private propertiesLoading?: azdata.LoadingComponent;
|
private propertiesLoading!: azdata.LoadingComponent;
|
||||||
private kibanaLoading?: azdata.LoadingComponent;
|
private kibanaLoading!: azdata.LoadingComponent;
|
||||||
private grafanaLoading?: azdata.LoadingComponent;
|
private grafanaLoading!: azdata.LoadingComponent;
|
||||||
private nodesTableLoading?: azdata.LoadingComponent;
|
|
||||||
|
|
||||||
private properties?: azdata.PropertiesContainerComponent;
|
private properties!: azdata.PropertiesContainerComponent;
|
||||||
private kibanaLink?: azdata.HyperlinkComponent;
|
private kibanaLink!: azdata.HyperlinkComponent;
|
||||||
private grafanaLink?: azdata.HyperlinkComponent;
|
private grafanaLink!: azdata.HyperlinkComponent;
|
||||||
private nodesTable?: azdata.DeclarativeTableComponent;
|
|
||||||
|
private readonly _azdataApi: azdataExt.IExtension;
|
||||||
|
|
||||||
constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
|
constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
|
||||||
super(modelView);
|
super(modelView);
|
||||||
|
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
|
||||||
|
|
||||||
this.disposables.push(
|
this.disposables.push(
|
||||||
this._controllerModel.onEndpointsUpdated(() => this.eventuallyRunOnInitialized(() => this.handleEndpointsUpdated())),
|
|
||||||
this._controllerModel.onRegistrationsUpdated(() => this.eventuallyRunOnInitialized(() => this.handleRegistrationsUpdated())),
|
this._controllerModel.onRegistrationsUpdated(() => this.eventuallyRunOnInitialized(() => this.handleRegistrationsUpdated())),
|
||||||
this._postgresModel.onServiceUpdated(() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())),
|
this._postgresModel.onConfigUpdated(() => this.eventuallyRunOnInitialized(() => this.handleConfigUpdated())));
|
||||||
this._postgresModel.onPodsUpdated(() => this.eventuallyRunOnInitialized(() => this.handlePodsUpdated())));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get title(): string {
|
protected get title(): string {
|
||||||
@@ -60,7 +61,7 @@ export class PostgresOverviewPage extends DashboardPage {
|
|||||||
this.propertiesLoading = this.modelView.modelBuilder.loadingComponent()
|
this.propertiesLoading = this.modelView.modelBuilder.loadingComponent()
|
||||||
.withItem(this.properties)
|
.withItem(this.properties)
|
||||||
.withProperties<azdata.LoadingComponentProperties>({
|
.withProperties<azdata.LoadingComponentProperties>({
|
||||||
loading: !this._controllerModel.registrationsLastUpdated && !this._postgresModel.serviceLastUpdated && !this._postgresModel.podsLastUpdated
|
loading: !this._controllerModel.registrationsLastUpdated && !this._postgresModel.configLastUpdated
|
||||||
}).component();
|
}).component();
|
||||||
|
|
||||||
content.addItem(this.propertiesLoading, { CSSStyles: cssStyles.text });
|
content.addItem(this.propertiesLoading, { CSSStyles: cssStyles.text });
|
||||||
@@ -72,29 +73,26 @@ export class PostgresOverviewPage extends DashboardPage {
|
|||||||
CSSStyles: titleCSS
|
CSSStyles: titleCSS
|
||||||
}).component());
|
}).component());
|
||||||
|
|
||||||
this.kibanaLink = this.modelView.modelBuilder.hyperlink()
|
this.kibanaLink = this.modelView.modelBuilder.hyperlink().component();
|
||||||
.withProperties<azdata.HyperlinkComponentProperties>({
|
|
||||||
label: this.getKibanaLink(),
|
|
||||||
url: this.getKibanaLink()
|
|
||||||
}).component();
|
|
||||||
|
|
||||||
this.grafanaLink = this.modelView.modelBuilder.hyperlink()
|
this.grafanaLink = this.modelView.modelBuilder.hyperlink().component();
|
||||||
.withProperties<azdata.HyperlinkComponentProperties>({
|
|
||||||
label: this.getGrafanaLink(),
|
|
||||||
url: this.getGrafanaLink()
|
|
||||||
}).component();
|
|
||||||
|
|
||||||
this.kibanaLoading = this.modelView.modelBuilder.loadingComponent()
|
this.kibanaLoading = this.modelView.modelBuilder.loadingComponent()
|
||||||
.withItem(this.kibanaLink)
|
.withProperties<azdata.LoadingComponentProperties>(
|
||||||
.withProperties<azdata.LoadingComponentProperties>({
|
{ loading: !this._postgresModel?.configLastUpdated }
|
||||||
loading: !this._controllerModel.endpointsLastUpdated
|
)
|
||||||
}).component();
|
.component();
|
||||||
|
|
||||||
this.grafanaLoading = this.modelView.modelBuilder.loadingComponent()
|
this.grafanaLoading = this.modelView.modelBuilder.loadingComponent()
|
||||||
.withItem(this.grafanaLink)
|
.withProperties<azdata.LoadingComponentProperties>(
|
||||||
.withProperties<azdata.LoadingComponentProperties>({
|
{ loading: !this._postgresModel?.configLastUpdated }
|
||||||
loading: !this._controllerModel.endpointsLastUpdated
|
)
|
||||||
}).component();
|
.component();
|
||||||
|
|
||||||
|
this.refreshDashboardLinks();
|
||||||
|
|
||||||
|
this.kibanaLoading.component = this.kibanaLink;
|
||||||
|
this.grafanaLoading.component = this.grafanaLink;
|
||||||
|
|
||||||
const endpointsTable = this.modelView.modelBuilder.declarativeTable().withProperties<azdata.DeclarativeTableProperties>({
|
const endpointsTable = this.modelView.modelBuilder.declarativeTable().withProperties<azdata.DeclarativeTableProperties>({
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -134,60 +132,8 @@ export class PostgresOverviewPage extends DashboardPage {
|
|||||||
[loc.kibanaDashboard, this.kibanaLoading, loc.kibanaDashboardDescription],
|
[loc.kibanaDashboard, this.kibanaLoading, loc.kibanaDashboardDescription],
|
||||||
[loc.grafanaDashboard, this.grafanaLoading, loc.grafanaDashboardDescription]]
|
[loc.grafanaDashboard, this.grafanaLoading, loc.grafanaDashboardDescription]]
|
||||||
}).component();
|
}).component();
|
||||||
|
|
||||||
content.addItem(endpointsTable);
|
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;
|
this.initialized = true;
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
@@ -205,11 +151,13 @@ export class PostgresOverviewPage extends DashboardPage {
|
|||||||
try {
|
try {
|
||||||
const password = await promptAndConfirmPassword(input => !input ? loc.enterANonEmptyPassword : '');
|
const password = await promptAndConfirmPassword(input => !input ? loc.enterANonEmptyPassword : '');
|
||||||
if (password) {
|
if (password) {
|
||||||
await this._postgresModel.update(s => {
|
await this._azdataApi.azdata.arc.postgres.server.edit(
|
||||||
// TODO chgagnon
|
this._postgresModel.info.name,
|
||||||
// s.arc = s.arc ?? new DuskyObjectModelsDatabaseServiceArcPayload();
|
{
|
||||||
s.arc.servicePassword = password;
|
adminPassword: true,
|
||||||
});
|
noWait: true
|
||||||
|
},
|
||||||
|
{ 'AZDATA_PASSWORD': password });
|
||||||
vscode.window.showInformationMessage(loc.passwordReset);
|
vscode.window.showInformationMessage(loc.passwordReset);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -229,15 +177,22 @@ export class PostgresOverviewPage extends DashboardPage {
|
|||||||
deleteButton.onDidClick(async () => {
|
deleteButton.onDidClick(async () => {
|
||||||
deleteButton.enabled = false;
|
deleteButton.enabled = false;
|
||||||
try {
|
try {
|
||||||
/*
|
if (await promptForInstanceDeletion(this._postgresModel.info.name)) {
|
||||||
if (await promptForResourceDeletion(this._postgresModel.namespace, this._postgresModel.name)) {
|
await vscode.window.withProgress(
|
||||||
await this._postgresModel.delete();
|
{
|
||||||
await this._controllerModel.deleteRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name);
|
location: vscode.ProgressLocation.Notification,
|
||||||
vscode.window.showInformationMessage(loc.resourceDeleted(this._postgresModel.fullName));
|
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) {
|
} catch (error) {
|
||||||
vscode.window.showErrorMessage(loc.resourceDeletionFailed(this._postgresModel.fullName, error));
|
vscode.window.showErrorMessage(loc.instanceDeletionFailed(this._postgresModel.info.name, error));
|
||||||
} finally {
|
} finally {
|
||||||
deleteButton.enabled = true;
|
deleteButton.enabled = true;
|
||||||
}
|
}
|
||||||
@@ -256,7 +211,6 @@ export class PostgresOverviewPage extends DashboardPage {
|
|||||||
this.propertiesLoading!.loading = true;
|
this.propertiesLoading!.loading = true;
|
||||||
this.kibanaLoading!.loading = true;
|
this.kibanaLoading!.loading = true;
|
||||||
this.grafanaLoading!.loading = true;
|
this.grafanaLoading!.loading = true;
|
||||||
this.nodesTableLoading!.loading = true;
|
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this._postgresModel.refresh(),
|
this._postgresModel.refresh(),
|
||||||
@@ -278,15 +232,13 @@ export class PostgresOverviewPage extends DashboardPage {
|
|||||||
|
|
||||||
this.disposables.push(
|
this.disposables.push(
|
||||||
openInAzurePortalButton.onDidClick(async () => {
|
openInAzurePortalButton.onDidClick(async () => {
|
||||||
/*
|
const azure = this._controllerModel.controllerConfig?.spec.settings.azure;
|
||||||
const r = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name);
|
if (azure) {
|
||||||
if (!r) {
|
|
||||||
vscode.window.showErrorMessage(loc.couldNotFindAzureResource(this._postgresModel.fullName));
|
|
||||||
} else {
|
|
||||||
vscode.env.openExternal(vscode.Uri.parse(
|
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([
|
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([
|
||||||
@@ -298,63 +250,35 @@ export class PostgresOverviewPage extends DashboardPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getProperties(): azdata.PropertiesContainerItem[] {
|
private getProperties(): azdata.PropertiesContainerItem[] {
|
||||||
/*
|
const status = this._postgresModel.config?.status;
|
||||||
const registration = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name);
|
const azure = this._controllerModel.controllerConfig?.spec.settings.azure;
|
||||||
const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ displayName: loc.name, value: this._postgresModel.name },
|
{ displayName: loc.resourceGroup, value: azure?.resourceGroup || '-' },
|
||||||
{ displayName: loc.coordinatorEndpoint, value: `postgresql://postgres@${endpoint.ip}:${endpoint.port}` },
|
{ displayName: loc.dataController, value: this._controllerModel.controllerConfig?.metadata.name || '-' },
|
||||||
{ displayName: loc.status, value: this._postgresModel.service?.status?.state ?? '' },
|
{ 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.postgresAdminUsername, value: 'postgres' },
|
||||||
{ displayName: loc.dataController, value: this._controllerModel?.namespace ?? '' },
|
{ displayName: loc.postgresVersion, value: this._postgresModel.engineVersion ?? '-' },
|
||||||
{ displayName: loc.nodeConfiguration, value: this._postgresModel.configuration },
|
{ displayName: loc.nodeConfiguration, value: this._postgresModel.scaleConfiguration || '-' }
|
||||||
{ displayName: loc.subscriptionId, value: registration?.subscriptionId ?? '' },
|
|
||||||
{ displayName: loc.postgresVersion, value: this._postgresModel.service?.spec?.engine?.version?.toString() ?? '' }
|
|
||||||
];
|
];
|
||||||
*/
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getKibanaLink(): string {
|
private refreshDashboardLinks(): void {
|
||||||
const kibanaQuery = `kubernetes_namespace:"${this._postgresModel.namespace}" and custom_resource_name:"${this._postgresModel.name}"`;
|
if (this._postgresModel.config) {
|
||||||
return `${this._controllerModel.getEndpoint(Endpoints.logsui)?.endpoint}/app/kibana#/discover?_a=(query:(language:kuery,query:'${kibanaQuery}'))`;
|
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;
|
||||||
private getGrafanaLink(): string {
|
this.grafanaLink.url = grafanaUrl;
|
||||||
const grafanaQuery = `var-Namespace=${this._postgresModel.namespace}&var-Name=${this._postgresModel.name}`;
|
this.grafanaLoading.loading = false;
|
||||||
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() {
|
private handleRegistrationsUpdated() {
|
||||||
@@ -362,19 +286,9 @@ export class PostgresOverviewPage extends DashboardPage {
|
|||||||
this.propertiesLoading!.loading = false;
|
this.propertiesLoading!.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleServiceUpdated() {
|
private handleConfigUpdated() {
|
||||||
this.properties!.propertyItems = this.getProperties();
|
this.properties!.propertyItems = this.getProperties();
|
||||||
this.propertiesLoading!.loading = false;
|
this.propertiesLoading!.loading = false;
|
||||||
|
this.refreshDashboardLinks();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import * as vscode from 'vscode';
|
|||||||
import * as azdata from 'azdata';
|
import * as azdata from 'azdata';
|
||||||
import * as loc from '../../../localizedConstants';
|
import * as loc from '../../../localizedConstants';
|
||||||
import { IconPathHelper, cssStyles } from '../../../constants';
|
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 { DashboardPage } from '../../components/dashboardPage';
|
||||||
import { ControllerModel } from '../../../models/controllerModel';
|
import { ControllerModel } from '../../../models/controllerModel';
|
||||||
import { PostgresModel } from '../../../models/postgresModel';
|
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) {
|
constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
|
||||||
super(modelView);
|
super(modelView);
|
||||||
|
|
||||||
this.disposables.push(this._postgresModel.onServiceUpdated(
|
this.disposables.push(this._postgresModel.onConfigUpdated(
|
||||||
() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())));
|
() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())));
|
||||||
|
|
||||||
this.disposables.push(this._controllerModel.onRegistrationsUpdated(
|
this.disposables.push(this._controllerModel.onRegistrationsUpdated(
|
||||||
@@ -54,7 +54,7 @@ export class PostgresPropertiesPage extends DashboardPage {
|
|||||||
this.loading = this.modelView.modelBuilder.loadingComponent()
|
this.loading = this.modelView.modelBuilder.loadingComponent()
|
||||||
.withItem(this.keyValueContainer.container)
|
.withItem(this.keyValueContainer.container)
|
||||||
.withProperties<azdata.LoadingComponentProperties>({
|
.withProperties<azdata.LoadingComponentProperties>({
|
||||||
loading: !this._postgresModel.serviceLastUpdated && !this._controllerModel.registrationsLastUpdated
|
loading: !this._postgresModel.configLastUpdated && !this._controllerModel.registrationsLastUpdated
|
||||||
}).component();
|
}).component();
|
||||||
|
|
||||||
content.addItem(this.loading);
|
content.addItem(this.loading);
|
||||||
@@ -91,24 +91,20 @@ export class PostgresPropertiesPage extends DashboardPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getProperties(): KeyValue[] {
|
private getProperties(): KeyValue[] {
|
||||||
/*
|
const endpoint = this._postgresModel.endpoint;
|
||||||
const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint;
|
const status = this._postgresModel.config?.status;
|
||||||
const connectionString = `postgresql://postgres@${endpoint.ip}:${endpoint.port}`;
|
|
||||||
const registration = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name);
|
|
||||||
|
|
||||||
return [
|
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 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
|
// 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.dataController, this._controllerModel.controllerConfig?.metadata.namespace ?? ''),
|
||||||
new TextKeyValue(this.modelView.modelBuilder, loc.nodeConfiguration, this._postgresModel.configuration),
|
new TextKeyValue(this.modelView.modelBuilder, loc.nodeConfiguration, this._postgresModel.scaleConfiguration ?? ''),
|
||||||
new TextKeyValue(this.modelView.modelBuilder, loc.postgresVersion, this._postgresModel.service?.spec?.engine?.version?.toString() ?? ''),
|
new TextKeyValue(this.modelView.modelBuilder, loc.postgresVersion, this._postgresModel.engineVersion ?? ''),
|
||||||
new TextKeyValue(this.modelView.modelBuilder, loc.resourceGroup, registration?.resourceGroupName ?? ''),
|
new TextKeyValue(this.modelView.modelBuilder, loc.resourceGroup, this._controllerModel.controllerConfig?.spec.settings.azure.resourceGroup ?? ''),
|
||||||
new TextKeyValue(this.modelView.modelBuilder, loc.subscriptionId, registration?.subscriptionId ?? '')
|
new TextKeyValue(this.modelView.modelBuilder, loc.subscriptionId, this._controllerModel.controllerConfig?.spec.settings.azure.subscription ?? '')
|
||||||
];
|
];
|
||||||
*/
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleRegistrationsUpdated() {
|
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.
|
* 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 azdata from 'azdata';
|
||||||
import * as loc from '../../../localizedConstants';
|
import * as loc from '../../../localizedConstants';
|
||||||
import { IconPathHelper, cssStyles } from '../../../constants';
|
import { IconPathHelper, cssStyles } from '../../../constants';
|
||||||
import { DashboardPage } from '../../components/dashboardPage';
|
import { DashboardPage } from '../../components/dashboardPage';
|
||||||
|
import { ControllerModel } from '../../../models/controllerModel';
|
||||||
|
import { ResourceType } from 'arc';
|
||||||
|
import { PostgresModel } from '../../../models/postgresModel';
|
||||||
|
|
||||||
export class PostgresSupportRequestPage extends DashboardPage {
|
export class PostgresSupportRequestPage extends DashboardPage {
|
||||||
constructor(protected modelView: azdata.ModelView) {
|
constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
|
||||||
super(modelView);
|
super(modelView);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,15 +52,13 @@ export class PostgresSupportRequestPage extends DashboardPage {
|
|||||||
|
|
||||||
this.disposables.push(
|
this.disposables.push(
|
||||||
supportRequestButton.onDidClick(() => {
|
supportRequestButton.onDidClick(() => {
|
||||||
/*
|
const azure = this._controllerModel.controllerConfig?.spec.settings.azure;
|
||||||
const r = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name);
|
if (azure) {
|
||||||
if (!r) {
|
|
||||||
vscode.window.showErrorMessage(loc.couldNotFindAzureResource(this._postgresModel.fullName));
|
|
||||||
} else {
|
|
||||||
vscode.env.openExternal(vscode.Uri.parse(
|
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);
|
content.addItem(supportRequestButton);
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
* 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 azdata from 'azdata';
|
||||||
|
import * as azdataExt from 'azdata-ext';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { Deferred } from '../../common/promise';
|
import { Deferred } from '../../common/promise';
|
||||||
@@ -12,94 +13,138 @@ import * as loc from '../../localizedConstants';
|
|||||||
import { ControllerModel } from '../../models/controllerModel';
|
import { ControllerModel } from '../../models/controllerModel';
|
||||||
import { InitializingComponent } from '../components/initializingComponent';
|
import { InitializingComponent } from '../components/initializingComponent';
|
||||||
import { AzureArcTreeDataProvider } from '../tree/azureArcTreeDataProvider';
|
import { AzureArcTreeDataProvider } from '../tree/azureArcTreeDataProvider';
|
||||||
|
import { getErrorMessage } from '../../common/utils';
|
||||||
|
|
||||||
export type ConnectToControllerDialogModel = { controllerModel: ControllerModel, password: string };
|
export type ConnectToControllerDialogModel = { controllerModel: ControllerModel, password: string };
|
||||||
export class ConnectToControllerDialog extends InitializingComponent {
|
abstract class ControllerDialogBase extends InitializingComponent {
|
||||||
private modelBuilder!: azdata.ModelBuilder;
|
protected modelBuilder!: azdata.ModelBuilder;
|
||||||
|
protected dialog: azdata.window.Dialog;
|
||||||
|
|
||||||
private urlInputBox!: azdata.InputBoxComponent;
|
protected urlInputBox!: azdata.InputBoxComponent;
|
||||||
private nameInputBox!: azdata.InputBoxComponent;
|
protected nameInputBox!: azdata.InputBoxComponent;
|
||||||
private usernameInputBox!: azdata.InputBoxComponent;
|
protected usernameInputBox!: azdata.InputBoxComponent;
|
||||||
private passwordInputBox!: azdata.InputBoxComponent;
|
protected passwordInputBox!: azdata.InputBoxComponent;
|
||||||
private rememberPwCheckBox!: azdata.CheckBoxComponent;
|
|
||||||
|
|
||||||
private _completionPromise = new Deferred<ConnectToControllerDialogModel | undefined>();
|
protected getComponents(): (azdata.FormComponent<azdata.Component> & { layout?: azdata.FormItemLayout | undefined; })[] {
|
||||||
|
return [
|
||||||
private _id!: string;
|
{
|
||||||
|
component: this.urlInputBox,
|
||||||
constructor(private _treeDataProvider: AzureArcTreeDataProvider) {
|
title: loc.controllerUrl,
|
||||||
super();
|
required: true
|
||||||
|
}, {
|
||||||
|
component: this.nameInputBox,
|
||||||
|
title: loc.controllerName,
|
||||||
|
required: false
|
||||||
|
}, {
|
||||||
|
component: this.usernameInputBox,
|
||||||
|
title: loc.username,
|
||||||
|
required: true
|
||||||
|
}, {
|
||||||
|
component: this.passwordInputBox,
|
||||||
|
title: loc.password,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public showDialog(controllerInfo?: ControllerInfo, password?: string): azdata.window.Dialog {
|
protected abstract fieldToFocusOn(): azdata.Component;
|
||||||
this._id = controllerInfo?.id ?? uuid();
|
protected readonlyFields(): azdata.InputBoxComponent[] { return []; }
|
||||||
const dialog = azdata.window.createModelViewDialog(loc.connectToController);
|
|
||||||
dialog.cancelButton.onClick(() => this.handleCancel());
|
|
||||||
dialog.registerContent(async view => {
|
|
||||||
this.modelBuilder = view.modelBuilder;
|
|
||||||
|
|
||||||
this.urlInputBox = this.modelBuilder.inputBox()
|
protected initializeFields(controllerInfo: ControllerInfo | undefined, password: string | undefined) {
|
||||||
.withProperties<azdata.InputBoxProperties>({
|
this.urlInputBox = this.modelBuilder.inputBox()
|
||||||
value: controllerInfo?.url,
|
.withProperties<azdata.InputBoxProperties>({
|
||||||
// If we have a model then we're editing an existing connection so don't let them modify the URL
|
value: controllerInfo?.url,
|
||||||
readOnly: !!controllerInfo
|
// If we have a model then we're editing an existing connection so don't let them modify the URL
|
||||||
}).component();
|
readOnly: !!controllerInfo
|
||||||
this.nameInputBox = this.modelBuilder.inputBox()
|
}).component();
|
||||||
.withProperties<azdata.InputBoxProperties>({
|
this.nameInputBox = this.modelBuilder.inputBox()
|
||||||
value: controllerInfo?.name
|
.withProperties<azdata.InputBoxProperties>({
|
||||||
}).component();
|
value: controllerInfo?.name
|
||||||
this.usernameInputBox = this.modelBuilder.inputBox()
|
}).component();
|
||||||
.withProperties<azdata.InputBoxProperties>({
|
this.usernameInputBox = this.modelBuilder.inputBox()
|
||||||
value: controllerInfo?.username
|
.withProperties<azdata.InputBoxProperties>({
|
||||||
}).component();
|
value: controllerInfo?.username
|
||||||
this.passwordInputBox = this.modelBuilder.inputBox()
|
}).component();
|
||||||
.withProperties<azdata.InputBoxProperties>({
|
this.passwordInputBox = this.modelBuilder.inputBox()
|
||||||
inputType: 'password',
|
.withProperties<azdata.InputBoxProperties>({
|
||||||
value: password
|
inputType: 'password',
|
||||||
})
|
value: password
|
||||||
.component();
|
}).component();
|
||||||
this.rememberPwCheckBox = this.modelBuilder.checkBox()
|
}
|
||||||
.withProperties<azdata.CheckBoxProperties>({
|
|
||||||
label: loc.rememberPassword,
|
protected completionPromise = new Deferred<ConnectToControllerDialogModel | undefined>();
|
||||||
checked: controllerInfo?.rememberPassword
|
protected id!: string;
|
||||||
}).component();
|
protected resources: ResourceInfo[] = [];
|
||||||
|
|
||||||
|
constructor(protected treeDataProvider: AzureArcTreeDataProvider, title: string) {
|
||||||
|
super();
|
||||||
|
this.dialog = azdata.window.createModelViewDialog(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
this.initializeFields(controllerInfo, password);
|
||||||
|
|
||||||
let formModel = this.modelBuilder.formContainer()
|
let formModel = this.modelBuilder.formContainer()
|
||||||
.withFormItems([{
|
.withFormItems([{
|
||||||
components: [
|
components: this.getComponents(),
|
||||||
{
|
|
||||||
component: this.urlInputBox,
|
|
||||||
title: loc.controllerUrl,
|
|
||||||
required: true
|
|
||||||
}, {
|
|
||||||
component: this.nameInputBox,
|
|
||||||
title: loc.controllerName,
|
|
||||||
required: false
|
|
||||||
}, {
|
|
||||||
component: this.usernameInputBox,
|
|
||||||
title: loc.username,
|
|
||||||
required: true
|
|
||||||
}, {
|
|
||||||
component: this.passwordInputBox,
|
|
||||||
title: loc.password,
|
|
||||||
required: true
|
|
||||||
}, {
|
|
||||||
component: this.rememberPwCheckBox,
|
|
||||||
title: ''
|
|
||||||
}
|
|
||||||
],
|
|
||||||
title: ''
|
title: ''
|
||||||
}]).withLayout({ width: '100%' }).component();
|
}]).withLayout({ width: '100%' }).component();
|
||||||
await view.initializeModel(formModel);
|
await view.initializeModel(formModel);
|
||||||
this.urlInputBox.focus();
|
await this.fieldToFocusOn().focus();
|
||||||
|
this.readonlyFields().forEach(f => f.readOnly = true);
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
dialog.registerCloseValidator(async () => await this.validate());
|
this.dialog.registerCloseValidator(async () => await this.validate());
|
||||||
dialog.okButton.label = loc.connect;
|
this.dialog.okButton.label = loc.connect;
|
||||||
dialog.cancelButton.label = loc.cancel;
|
this.dialog.cancelButton.label = loc.cancel;
|
||||||
azdata.window.openDialog(dialog);
|
azdata.window.openDialog(this.dialog);
|
||||||
return dialog;
|
return this.dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract async validate(): Promise<boolean>;
|
||||||
|
|
||||||
|
private handleCancel(): void {
|
||||||
|
this.completionPromise.resolve(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
public waitForClose(): Promise<ConnectToControllerDialogModel | undefined> {
|
||||||
|
return this.completionPromise.promise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ConnectToControllerDialog extends ControllerDialogBase {
|
||||||
|
protected rememberPwCheckBox!: azdata.CheckBoxComponent;
|
||||||
|
|
||||||
|
protected fieldToFocusOn() {
|
||||||
|
return this.urlInputBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getComponents() {
|
||||||
|
return [
|
||||||
|
...super.getComponents(),
|
||||||
|
{
|
||||||
|
component: this.rememberPwCheckBox,
|
||||||
|
title: ''
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected initializeFields(controllerInfo: ControllerInfo | undefined, password: string | undefined) {
|
||||||
|
super.initializeFields(controllerInfo, password);
|
||||||
|
this.rememberPwCheckBox = this.modelBuilder.checkBox()
|
||||||
|
.withProperties<azdata.CheckBoxProperties>({
|
||||||
|
label: loc.rememberPassword,
|
||||||
|
checked: controllerInfo?.rememberPassword
|
||||||
|
}).component();
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(treeDataProvider: AzureArcTreeDataProvider) {
|
||||||
|
super(treeDataProvider, loc.connectToController);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async validate(): Promise<boolean> {
|
public async validate(): Promise<boolean> {
|
||||||
@@ -120,32 +165,86 @@ export class ConnectToControllerDialog extends InitializingComponent {
|
|||||||
url = `${url}:30080`;
|
url = `${url}:30080`;
|
||||||
}
|
}
|
||||||
const controllerInfo: ControllerInfo = {
|
const controllerInfo: ControllerInfo = {
|
||||||
id: this._id,
|
id: this.id,
|
||||||
url: url,
|
url: url,
|
||||||
name: this.nameInputBox.value ?? '',
|
name: this.nameInputBox.value ?? '',
|
||||||
username: this.usernameInputBox.value,
|
username: this.usernameInputBox.value,
|
||||||
rememberPassword: this.rememberPwCheckBox.checked ?? false,
|
rememberPassword: this.rememberPwCheckBox.checked ?? false,
|
||||||
resources: []
|
resources: this.resources
|
||||||
};
|
};
|
||||||
const controllerModel = new ControllerModel(this._treeDataProvider, controllerInfo, this.passwordInputBox.value);
|
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
|
||||||
try {
|
try {
|
||||||
// Validate that we can connect to the controller, this also populates the controllerRegistration from the connection response.
|
// Validate that we can connect to the controller, this also populates the controllerRegistration from the connection response.
|
||||||
await controllerModel.refresh(false);
|
await controllerModel.refresh(false);
|
||||||
// default info.name to the name of the controller instance if the user did not specify their own and to a pre-canned default if for some weird reason controller endpoint returned instanceName is also not a valid value
|
// default info.name to the name of the controller instance if the user did not specify their own and to a pre-canned default if for some weird reason controller endpoint returned instanceName is also not a valid value
|
||||||
controllerModel.info.name = controllerModel.info.name || controllerModel.controllerConfig?.metadata.name || loc.defaultControllerName;
|
controllerModel.info.name = controllerModel.info.name || controllerModel.controllerConfig?.metadata.name || loc.defaultControllerName;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
vscode.window.showErrorMessage(loc.connectToControllerFailed(this.urlInputBox.value, err));
|
this.dialog.message = {
|
||||||
|
text: loc.connectToControllerFailed(this.urlInputBox.value, err),
|
||||||
|
level: azdata.window.MessageLevel.Error
|
||||||
|
};
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this._completionPromise.resolve({ controllerModel: controllerModel, password: this.passwordInputBox.value });
|
this.completionPromise.resolve({ controllerModel: controllerModel, password: this.passwordInputBox.value });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class PasswordToControllerDialog extends ControllerDialogBase {
|
||||||
|
|
||||||
|
constructor(treeDataProvider: AzureArcTreeDataProvider) {
|
||||||
|
super(treeDataProvider, loc.passwordToController);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fieldToFocusOn() {
|
||||||
|
return this.passwordInputBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected readonlyFields() {
|
||||||
|
return [
|
||||||
|
this.urlInputBox,
|
||||||
|
this.nameInputBox,
|
||||||
|
this.usernameInputBox
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async validate(): Promise<boolean> {
|
||||||
|
if (!this.passwordInputBox.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const azdataApi = <azdataExt.IExtension>vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
|
||||||
|
try {
|
||||||
|
await azdataApi.azdata.login(this.urlInputBox.value!, this.usernameInputBox.value!, this.passwordInputBox.value);
|
||||||
|
} catch (e) {
|
||||||
|
if (getErrorMessage(e).match(/Wrong username or password/i)) {
|
||||||
|
this.dialog.message = {
|
||||||
|
text: loc.invalidPassword,
|
||||||
|
level: azdata.window.MessageLevel.Error
|
||||||
|
};
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
this.dialog.message = {
|
||||||
|
text: loc.errorVerifyingPassword(e),
|
||||||
|
level: azdata.window.MessageLevel.Error
|
||||||
|
};
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const controllerInfo: ControllerInfo = {
|
||||||
|
id: this.id,
|
||||||
|
url: this.urlInputBox.value!,
|
||||||
|
name: this.nameInputBox.value!,
|
||||||
|
username: this.usernameInputBox.value!,
|
||||||
|
rememberPassword: false,
|
||||||
|
resources: []
|
||||||
|
};
|
||||||
|
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
|
||||||
|
this.completionPromise.resolve({ controllerModel: controllerModel, password: this.passwordInputBox.value });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleCancel(): void {
|
public showDialog(controllerInfo?: ControllerInfo): azdata.window.Dialog {
|
||||||
this._completionPromise.resolve(undefined);
|
const dialog = super.showDialog(controllerInfo);
|
||||||
}
|
dialog.okButton.label = loc.ok;
|
||||||
|
return dialog;
|
||||||
public waitForClose(): Promise<ConnectToControllerDialogModel | undefined> {
|
|
||||||
return this._completionPromise.promise;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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) {
|
if (resourceNode) {
|
||||||
await resourceNode.openDashboard();
|
await resourceNode.openDashboard();
|
||||||
} else {
|
} 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 {
|
} 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.
|
* 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 * as vscode from 'vscode';
|
||||||
import { parseInstanceName, UserCancelledError } from '../../common/utils';
|
import { UserCancelledError } from '../../common/utils';
|
||||||
import * as loc from '../../localizedConstants';
|
import * as loc from '../../localizedConstants';
|
||||||
import { ControllerModel, Registration } from '../../models/controllerModel';
|
import { ControllerModel, Registration } from '../../models/controllerModel';
|
||||||
import { MiaaModel } from '../../models/miaaModel';
|
import { MiaaModel } from '../../models/miaaModel';
|
||||||
import { PostgresModel } from '../../models/postgresModel';
|
import { PostgresModel } from '../../models/postgresModel';
|
||||||
|
import { ResourceModel } from '../../models/resourceModel';
|
||||||
import { ControllerDashboard } from '../dashboards/controller/controllerDashboard';
|
import { ControllerDashboard } from '../dashboards/controller/controllerDashboard';
|
||||||
import { AzureArcTreeDataProvider } from './azureArcTreeDataProvider';
|
import { AzureArcTreeDataProvider } from './azureArcTreeDataProvider';
|
||||||
import { MiaaTreeNode } from './miaaTreeNode';
|
import { MiaaTreeNode } from './miaaTreeNode';
|
||||||
|
import { NoInstancesTreeNode } from './noInstancesTreeNode';
|
||||||
import { PostgresTreeNode } from './postgresTreeNode';
|
import { PostgresTreeNode } from './postgresTreeNode';
|
||||||
import { RefreshTreeNode } from './refreshTreeNode';
|
import { RefreshTreeNode } from './refreshTreeNode';
|
||||||
import { ResourceTreeNode } from './resourceTreeNode';
|
import { ResourceTreeNode } from './resourceTreeNode';
|
||||||
@@ -23,7 +25,7 @@ import { TreeNode } from './treeNode';
|
|||||||
*/
|
*/
|
||||||
export class ControllerTreeNode extends TreeNode {
|
export class ControllerTreeNode extends TreeNode {
|
||||||
|
|
||||||
private _children: ResourceTreeNode[] = [];
|
private _children: ResourceTreeNode<ResourceModel>[] = [];
|
||||||
|
|
||||||
constructor(public model: ControllerModel, private _context: vscode.ExtensionContext, private _treeDataProvider: AzureArcTreeDataProvider) {
|
constructor(public model: ControllerModel, private _context: vscode.ExtensionContext, private _treeDataProvider: AzureArcTreeDataProvider) {
|
||||||
super(model.label, vscode.TreeItemCollapsibleState.Collapsed, ResourceType.dataControllers);
|
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> {
|
public async openDashboard(): Promise<void> {
|
||||||
@@ -68,14 +70,14 @@ export class ControllerTreeNode extends TreeNode {
|
|||||||
* @param resourceType The resourceType of the node
|
* @param resourceType The resourceType of the node
|
||||||
* @param name The name 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 =>
|
return this._children.find(c =>
|
||||||
c.model?.info.resourceType === resourceType &&
|
c.model?.info.resourceType === resourceType &&
|
||||||
c.model.info.name === name);
|
c.model.info.name === name);
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateChildren(registrations: Registration[]): void {
|
private updateChildren(registrations: Registration[]): void {
|
||||||
const newChildren: ResourceTreeNode[] = [];
|
const newChildren: ResourceTreeNode<ResourceModel>[] = [];
|
||||||
registrations.forEach(registration => {
|
registrations.forEach(registration => {
|
||||||
if (!registration.instanceName) {
|
if (!registration.instanceName) {
|
||||||
console.warn('Registration is missing required name value, skipping');
|
console.warn('Registration is missing required name value, skipping');
|
||||||
@@ -83,7 +85,7 @@ export class ControllerTreeNode extends TreeNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resourceInfo: ResourceInfo = {
|
const resourceInfo: ResourceInfo = {
|
||||||
name: parseInstanceName(registration.instanceName),
|
name: registration.instanceName,
|
||||||
resourceType: registration.instanceType ?? ''
|
resourceType: registration.instanceType ?? ''
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -100,10 +102,14 @@ export class ControllerTreeNode extends TreeNode {
|
|||||||
|
|
||||||
switch (registration.instanceType) {
|
switch (registration.instanceType) {
|
||||||
case ResourceType.postgresInstances:
|
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);
|
node = new PostgresTreeNode(postgresModel, this.model, this._context);
|
||||||
break;
|
break;
|
||||||
case ResourceType.sqlManagedInstances:
|
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);
|
const miaaModel = new MiaaModel(this.model, resourceInfo, registration, this._treeDataProvider);
|
||||||
node = new MiaaTreeNode(miaaModel, this.model);
|
node = new MiaaTreeNode(miaaModel, this.model);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ import * as vscode from 'vscode';
|
|||||||
import { ControllerModel } from '../../models/controllerModel';
|
import { ControllerModel } from '../../models/controllerModel';
|
||||||
import { MiaaModel } from '../../models/miaaModel';
|
import { MiaaModel } from '../../models/miaaModel';
|
||||||
import { MiaaDashboard } from '../dashboards/miaa/miaaDashboard';
|
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
|
* 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) {
|
constructor(model: MiaaModel, private _controllerModel: ControllerModel) {
|
||||||
super(model.info.name, vscode.TreeItemCollapsibleState.None, ResourceType.sqlManagedInstances);
|
super(model.info.name, vscode.TreeItemCollapsibleState.None, ResourceType.sqlManagedInstances, model);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async openDashboard(): Promise<void> {
|
public async openDashboard(): Promise<void> {
|
||||||
|
|||||||
@@ -2,16 +2,17 @@
|
|||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
import * as arc from 'arc';
|
|
||||||
import * as vscode from 'vscode';
|
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 {
|
||||||
|
|
||||||
export class ArcService {
|
|
||||||
private _arcApi: arc.IExtension;
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this._arcApi = vscode.extensions.getExtension(arc.extension.name)?.exports;
|
super(loc.noInstancesAvailable, vscode.TreeItemCollapsibleState.None, '');
|
||||||
}
|
|
||||||
|
|
||||||
public async getRegisteredDataControllers(): Promise<arc.ControllerInfo[]> {
|
|
||||||
return await this._arcApi.getRegisteredDataControllers();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,14 +13,14 @@ import { ResourceTreeNode } from './resourceTreeNode';
|
|||||||
/**
|
/**
|
||||||
* The TreeNode for displaying an Postgres Server group
|
* 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) {
|
constructor(model: PostgresModel, private _controllerModel: ControllerModel, private _context: vscode.ExtensionContext) {
|
||||||
super(_model.name, vscode.TreeItemCollapsibleState.None, ResourceType.postgresInstances, _model);
|
super(model.info.name, vscode.TreeItemCollapsibleState.None, ResourceType.postgresInstances, model);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async openDashboard(): Promise<void> {
|
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();
|
await postgresDashboard.showDashboard();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { refreshActionId } from '../../constants';
|
|||||||
export class RefreshTreeNode extends TreeNode {
|
export class RefreshTreeNode extends TreeNode {
|
||||||
|
|
||||||
constructor(private _parent: TreeNode) {
|
constructor(private _parent: TreeNode) {
|
||||||
super(loc.refreshToEnterCredentials, vscode.TreeItemCollapsibleState.None, 'refresh');
|
super(loc.refreshToEnterCredentials, vscode.TreeItemCollapsibleState.None, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
public command: vscode.Command = {
|
public command: vscode.Command = {
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import { TreeNode } from './treeNode';
|
|||||||
/**
|
/**
|
||||||
* A TreeNode belonging to a child of a Controller
|
* A TreeNode belonging to a child of a Controller
|
||||||
*/
|
*/
|
||||||
export abstract class ResourceTreeNode extends TreeNode {
|
export abstract class ResourceTreeNode<M extends ResourceModel> extends TreeNode {
|
||||||
constructor(label: string, collapsibleState: vscode.TreeItemCollapsibleState, resourceType?: string, public model?: ResourceModel) {
|
constructor(label: string, collapsibleState: vscode.TreeItemCollapsibleState, resourceType: string, public model: M) {
|
||||||
super(label, collapsibleState, resourceType);
|
super(label, collapsibleState, resourceType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
"light": "./images/sqldb_edge.svg",
|
"light": "./images/sqldb_edge.svg",
|
||||||
"dark": "./images/sqldb_edge_inverse.svg"
|
"dark": "./images/sqldb_edge_inverse.svg"
|
||||||
},
|
},
|
||||||
|
"tags": ["Hybrid", "SQL Server"],
|
||||||
"options": [
|
"options": [
|
||||||
{
|
{
|
||||||
"name": "type",
|
"name": "type",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Welcome to Microsoft Azure Data CLI Extension for Azure Data Studio!
|
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
|
## 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",
|
"name": "azdata",
|
||||||
"displayName": "%azdata.displayName%",
|
"displayName": "%azdata.displayName%",
|
||||||
"description": "%azdata.description%",
|
"description": "%azdata.description%",
|
||||||
"version": "0.1.2",
|
"version": "0.3.1",
|
||||||
"publisher": "Microsoft",
|
"publisher": "Microsoft",
|
||||||
"preview": true,
|
"preview": true,
|
||||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
|
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
|
||||||
"icon": "images/extension.png",
|
"icon": "images/extension.png",
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "*",
|
"vscode": "*",
|
||||||
"azdata": ">=1.20.0"
|
"azdata": ">=1.22.0"
|
||||||
},
|
},
|
||||||
"activationEvents": [
|
"activationEvents": [
|
||||||
"*"
|
"*"
|
||||||
@@ -18,6 +18,9 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/Microsoft/azuredatastudio.git"
|
"url": "https://github.com/Microsoft/azuredatastudio.git"
|
||||||
},
|
},
|
||||||
|
"extensionDependencies": [
|
||||||
|
"microsoft.resource-deployment"
|
||||||
|
],
|
||||||
"main": "./out/extension",
|
"main": "./out/extension",
|
||||||
"contributes": {
|
"contributes": {
|
||||||
"configuration": [
|
"configuration": [
|
||||||
@@ -129,5 +132,10 @@
|
|||||||
"sinon": "^9.0.2",
|
"sinon": "^9.0.2",
|
||||||
"typemoq": "^2.1.0",
|
"typemoq": "^2.1.0",
|
||||||
"vscodetestcover": "^1.1.0"
|
"vscodetestcover": "^1.1.0"
|
||||||
|
},
|
||||||
|
"__metadata": {
|
||||||
|
"id": "73",
|
||||||
|
"publisherDisplayName": "Microsoft",
|
||||||
|
"publisherId": "Microsoft"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
153
extensions/azdata/src/api.ts
Normal file
153
extensions/azdata/src/api.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* 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';
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import { IAzdataTool, isEulaAccepted, promptForEula } from './azdata';
|
||||||
|
import Logger from './common/logger';
|
||||||
|
import { NoAzdataError } from './common/utils';
|
||||||
|
import * as constants from './constants';
|
||||||
|
import * as loc from './localizedConstants';
|
||||||
|
import { AzdataToolService } from './services/azdataToolService';
|
||||||
|
|
||||||
|
function throwIfNoAzdataOrEulaNotAccepted(azdata: IAzdataTool | undefined, eulaAccepted: boolean): asserts azdata {
|
||||||
|
throwIfNoAzdata(azdata);
|
||||||
|
if (!eulaAccepted) {
|
||||||
|
Logger.log(loc.eulaNotAccepted);
|
||||||
|
throw new Error(loc.eulaNotAccepted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function throwIfNoAzdata(localAzdata: IAzdataTool | undefined): asserts localAzdata {
|
||||||
|
if (!localAzdata) {
|
||||||
|
Logger.log(loc.noAzdata);
|
||||||
|
throw new NoAzdataError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getExtensionApi(memento: vscode.Memento, azdataToolService: AzdataToolService, localAzdataDiscovered: Promise<IAzdataTool | undefined>): azdataExt.IExtension {
|
||||||
|
return {
|
||||||
|
isEulaAccepted: async () => {
|
||||||
|
throwIfNoAzdata(await localAzdataDiscovered); // ensure that we have discovered Azdata
|
||||||
|
return !!memento.get<boolean>(constants.eulaAccepted);
|
||||||
|
},
|
||||||
|
promptForEula: async (requireUserAction: boolean = true): Promise<boolean> => {
|
||||||
|
await localAzdataDiscovered;
|
||||||
|
return promptForEula(memento, true /* userRequested */, requireUserAction);
|
||||||
|
},
|
||||||
|
azdata: getAzdataApi(localAzdataDiscovered, azdataToolService, memento)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefined>, azdataToolService: AzdataToolService, memento: vscode.Memento): azdataExt.IAzdataApi {
|
||||||
|
return {
|
||||||
|
arc: {
|
||||||
|
dc: {
|
||||||
|
create: async (namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string) => {
|
||||||
|
await localAzdataDiscovered;
|
||||||
|
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||||
|
return azdataToolService.localAzdata.arc.dc.create(namespace, name, connectivityMode, resourceGroup, location, subscription, profileName, storageClass);
|
||||||
|
},
|
||||||
|
endpoint: {
|
||||||
|
list: async () => {
|
||||||
|
await localAzdataDiscovered;
|
||||||
|
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||||
|
return azdataToolService.localAzdata.arc.dc.endpoint.list();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
list: async () => {
|
||||||
|
await localAzdataDiscovered;
|
||||||
|
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||||
|
return azdataToolService.localAzdata.arc.dc.config.list();
|
||||||
|
},
|
||||||
|
show: async () => {
|
||||||
|
await localAzdataDiscovered;
|
||||||
|
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||||
|
return azdataToolService.localAzdata.arc.dc.config.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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));
|
||||||
|
return azdataToolService.localAzdata.arc.postgres.server.list();
|
||||||
|
},
|
||||||
|
show: async (name: string) => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sql: {
|
||||||
|
mi: {
|
||||||
|
delete: async (name: string) => {
|
||||||
|
await localAzdataDiscovered;
|
||||||
|
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||||
|
return azdataToolService.localAzdata.arc.sql.mi.delete(name);
|
||||||
|
},
|
||||||
|
list: async () => {
|
||||||
|
await localAzdataDiscovered;
|
||||||
|
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||||
|
return azdataToolService.localAzdata.arc.sql.mi.list();
|
||||||
|
},
|
||||||
|
show: async (name: string) => {
|
||||||
|
await localAzdataDiscovered;
|
||||||
|
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||||
|
return azdataToolService.localAzdata.arc.sql.mi.show(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getPath: async () => {
|
||||||
|
await localAzdataDiscovered;
|
||||||
|
throwIfNoAzdata(azdataToolService.localAzdata);
|
||||||
|
return azdataToolService.localAzdata.getPath();
|
||||||
|
},
|
||||||
|
login: async (endpoint: string, username: string, password: string) => {
|
||||||
|
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||||
|
return azdataToolService.localAzdata.login(endpoint, username, password);
|
||||||
|
},
|
||||||
|
getSemVersion: async () => {
|
||||||
|
await localAzdataDiscovered;
|
||||||
|
throwIfNoAzdata(azdataToolService.localAzdata);
|
||||||
|
return azdataToolService.localAzdata.getSemVersion();
|
||||||
|
},
|
||||||
|
version: async () => {
|
||||||
|
await localAzdataDiscovered;
|
||||||
|
throwIfNoAzdata(azdataToolService.localAzdata);
|
||||||
|
return azdataToolService.localAzdata.version();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@@ -4,16 +4,18 @@
|
|||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import * as azdataExt from 'azdata-ext';
|
import * as azdataExt from 'azdata-ext';
|
||||||
|
import * as fs from 'fs';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
|
import * as path from 'path';
|
||||||
import { SemVer } from 'semver';
|
import { SemVer } from 'semver';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
|
import { getPlatformDownloadLink, getPlatformReleaseVersion } from './azdataReleaseInfo';
|
||||||
import { executeCommand, executeSudoCommand, ExitCodeError, ProcessOutput } from './common/childProcess';
|
import { executeCommand, executeSudoCommand, ExitCodeError, ProcessOutput } from './common/childProcess';
|
||||||
import { HttpClient } from './common/httpClient';
|
import { HttpClient } from './common/httpClient';
|
||||||
import Logger from './common/logger';
|
import Logger from './common/logger';
|
||||||
import { getErrorMessage, searchForCmd } from './common/utils';
|
import { getErrorMessage, NoAzdataError, searchForCmd } from './common/utils';
|
||||||
import { azdataAcceptEulaKey, azdataConfigSection, azdataFound, azdataHostname, azdataInstallKey, azdataReleaseJson, azdataUpdateKey, azdataUri, debugConfigKey, eulaAccepted, eulaUrl, microsoftPrivacyStatementUrl } from './constants';
|
import { azdataAcceptEulaKey, azdataConfigSection, azdataFound, azdataInstallKey, azdataUpdateKey, debugConfigKey, eulaAccepted, eulaUrl, microsoftPrivacyStatementUrl } from './constants';
|
||||||
import * as loc from './localizedConstants';
|
import * as loc from './localizedConstants';
|
||||||
import * as fs from 'fs';
|
|
||||||
|
|
||||||
const enum AzdataDeployOption {
|
const enum AzdataDeployOption {
|
||||||
dontPrompt = 'dontPrompt',
|
dontPrompt = 'dontPrompt',
|
||||||
@@ -24,9 +26,6 @@ const enum AzdataDeployOption {
|
|||||||
* Interface for an object to interact with the azdata tool installed on the box.
|
* Interface for an object to interact with the azdata tool installed on the box.
|
||||||
*/
|
*/
|
||||||
export interface IAzdataTool extends azdataExt.IAzdataApi {
|
export interface IAzdataTool extends azdataExt.IAzdataApi {
|
||||||
path: string,
|
|
||||||
cachedVersion: SemVer
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes azdata with the specified arguments (e.g. --version) and returns the result
|
* Executes azdata with the specified arguments (e.g. --version) and returns the result
|
||||||
* @param args The args to pass to azdata
|
* @param args The args to pass to azdata
|
||||||
@@ -38,10 +37,27 @@ export interface IAzdataTool extends azdataExt.IAzdataApi {
|
|||||||
/**
|
/**
|
||||||
* An object to interact with the azdata tool installed on the box.
|
* An object to interact with the azdata tool installed on the box.
|
||||||
*/
|
*/
|
||||||
export class AzdataTool implements IAzdataTool {
|
export class AzdataTool implements azdataExt.IAzdataApi {
|
||||||
public cachedVersion: SemVer;
|
|
||||||
constructor(public path: string, version: string) {
|
private _semVersion: SemVer;
|
||||||
this.cachedVersion = new SemVer(version);
|
constructor(private _path: string, version: string) {
|
||||||
|
this._semVersion = new SemVer(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The semVersion corresponding to this installation of azdata. version() method should have been run
|
||||||
|
* before fetching this value to ensure that correct value is returned. This is almost always correct unless
|
||||||
|
* Azdata has gotten reinstalled in the background after this IAzdataApi object was constructed.
|
||||||
|
*/
|
||||||
|
public async getSemVersion(): Promise<SemVer> {
|
||||||
|
return this._semVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* gets the path where azdata tool is installed
|
||||||
|
*/
|
||||||
|
public async getPath(): Promise<string> {
|
||||||
|
return this._path;
|
||||||
}
|
}
|
||||||
|
|
||||||
public arc = {
|
public arc = {
|
||||||
@@ -78,11 +94,44 @@ export class AzdataTool implements IAzdataTool {
|
|||||||
},
|
},
|
||||||
postgres: {
|
postgres: {
|
||||||
server: {
|
server: {
|
||||||
|
delete: async (name: string) => {
|
||||||
|
return this.executeCommand<void>(['arc', 'postgres', 'server', 'delete', '-n', name]);
|
||||||
|
},
|
||||||
list: async () => {
|
list: async () => {
|
||||||
return this.executeCommand<azdataExt.PostgresServerListResult[]>(['arc', 'postgres', 'server', 'list']);
|
return this.executeCommand<azdataExt.PostgresServerListResult[]>(['arc', 'postgres', 'server', 'list']);
|
||||||
},
|
},
|
||||||
show: async (name: string) => {
|
show: async (name: string) => {
|
||||||
return this.executeCommand<azdataExt.PostgresServerShowResult>(['arc', 'postgres', 'server', 'show', '-n', name]);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -110,8 +159,8 @@ export class AzdataTool implements IAzdataTool {
|
|||||||
* It also updates the cachedVersion property based on the return value from the tool.
|
* It also updates the cachedVersion property based on the return value from the tool.
|
||||||
*/
|
*/
|
||||||
public async version(): Promise<azdataExt.AzdataOutput<string>> {
|
public async version(): Promise<azdataExt.AzdataOutput<string>> {
|
||||||
const output = await executeAzdataCommand(`"${this.path}"`, ['--version']);
|
const output = await executeAzdataCommand(`"${this._path}"`, ['--version']);
|
||||||
this.cachedVersion = new SemVer(parseVersion(output.stdout));
|
this._semVersion = new SemVer(parseVersion(output.stdout));
|
||||||
return {
|
return {
|
||||||
logs: [],
|
logs: [],
|
||||||
stdout: output.stdout.split(os.EOL),
|
stdout: output.stdout.split(os.EOL),
|
||||||
@@ -122,7 +171,7 @@ export class AzdataTool implements IAzdataTool {
|
|||||||
|
|
||||||
public async executeCommand<R>(args: string[], additionalEnvVars?: { [key: string]: string }): Promise<azdataExt.AzdataOutput<R>> {
|
public async executeCommand<R>(args: string[], additionalEnvVars?: { [key: string]: string }): Promise<azdataExt.AzdataOutput<R>> {
|
||||||
try {
|
try {
|
||||||
const output = JSON.parse((await executeAzdataCommand(`"${this.path}"`, args.concat(['--output', 'json']), additionalEnvVars)).stdout);
|
const output = JSON.parse((await executeAzdataCommand(`"${this._path}"`, args.concat(['--output', 'json']), additionalEnvVars)).stdout);
|
||||||
return {
|
return {
|
||||||
logs: <string[]>output.log,
|
logs: <string[]>output.log,
|
||||||
stdout: <string[]>output.stdout,
|
stdout: <string[]>output.stdout,
|
||||||
@@ -136,19 +185,19 @@ export class AzdataTool implements IAzdataTool {
|
|||||||
// to get the correct stderr out. The actual value we get is something like
|
// to get the correct stderr out. The actual value we get is something like
|
||||||
// ERROR: { stderr: '...' }
|
// ERROR: { stderr: '...' }
|
||||||
// so we also need to trim off the start that isn't a valid JSON blob
|
// 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('{'))).stderr;
|
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)
|
// 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.
|
// check if azdata still exists if it does then rethrow the original error if not then emit a new specific error.
|
||||||
try {
|
try {
|
||||||
await fs.promises.access(this.path);
|
await fs.promises.access(this._path);
|
||||||
//this.path exists
|
//this.path exists
|
||||||
throw err; // rethrow the error
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// this.path does not exist
|
// this.path does not exist
|
||||||
await vscode.commands.executeCommand('setContext', azdataFound, false);
|
await vscode.commands.executeCommand('setContext', azdataFound, false);
|
||||||
throw (loc.noAzdata);
|
throw new NoAzdataError();
|
||||||
}
|
}
|
||||||
|
throw err; // rethrow the original error
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -176,7 +225,7 @@ export async function findAzdata(): Promise<IAzdataTool> {
|
|||||||
try {
|
try {
|
||||||
const azdata = await findSpecificAzdata();
|
const azdata = await findSpecificAzdata();
|
||||||
await vscode.commands.executeCommand('setContext', azdataFound, true); // save a context key that azdata was found so that command for installing azdata is no longer available in commandPalette and that for updating it is.
|
await vscode.commands.executeCommand('setContext', azdataFound, true); // save a context key that azdata was found so that command for installing azdata is no longer available in commandPalette and that for updating it is.
|
||||||
Logger.log(loc.foundExistingAzdata(azdata.path, azdata.cachedVersion.raw));
|
Logger.log(loc.foundExistingAzdata(await azdata.getPath(), (await azdata.getSemVersion()).raw));
|
||||||
return azdata;
|
return azdata;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Logger.log(loc.couldNotFindAzdata(err));
|
Logger.log(loc.couldNotFindAzdata(err));
|
||||||
@@ -262,12 +311,12 @@ export async function checkAndInstallAzdata(userRequested: boolean = false): Pro
|
|||||||
*/
|
*/
|
||||||
export async function checkAndUpdateAzdata(currentAzdata?: IAzdataTool, userRequested: boolean = false): Promise<boolean> {
|
export async function checkAndUpdateAzdata(currentAzdata?: IAzdataTool, userRequested: boolean = false): Promise<boolean> {
|
||||||
if (currentAzdata !== undefined) {
|
if (currentAzdata !== undefined) {
|
||||||
const newVersion = await discoverLatestAvailableAzdataVersion();
|
const newSemVersion = await discoverLatestAvailableAzdataVersion();
|
||||||
if (newVersion.compare(currentAzdata.cachedVersion) === 1) {
|
if (newSemVersion.compare(await currentAzdata.getSemVersion()) === 1) {
|
||||||
Logger.log(loc.foundAzdataVersionToUpdateTo(newVersion.raw, currentAzdata.cachedVersion.raw));
|
Logger.log(loc.foundAzdataVersionToUpdateTo(newSemVersion.raw, (await currentAzdata.getSemVersion()).raw));
|
||||||
return await promptToUpdateAzdata(newVersion.raw, userRequested);
|
return await promptToUpdateAzdata(newSemVersion.raw, userRequested);
|
||||||
} else {
|
} else {
|
||||||
Logger.log(loc.currentlyInstalledVersionIsLatest(currentAzdata.cachedVersion.raw));
|
Logger.log(loc.currentlyInstalledVersionIsLatest((await currentAzdata.getSemVersion()).raw));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Logger.log(loc.updateCheckSkipped);
|
Logger.log(loc.updateCheckSkipped);
|
||||||
@@ -297,6 +346,7 @@ async function promptToInstallAzdata(userRequested: boolean = false): Promise<bo
|
|||||||
? [loc.yes, loc.no]
|
? [loc.yes, loc.no]
|
||||||
: [loc.yes, loc.askLater, loc.doNotAskAgain];
|
: [loc.yes, loc.askLater, loc.doNotAskAgain];
|
||||||
if (config === AzdataDeployOption.prompt) {
|
if (config === AzdataDeployOption.prompt) {
|
||||||
|
Logger.log(loc.promptForAzdataInstallLog);
|
||||||
response = await vscode.window.showErrorMessage(loc.promptForAzdataInstall, ...responses);
|
response = await vscode.window.showErrorMessage(loc.promptForAzdataInstall, ...responses);
|
||||||
Logger.log(loc.userResponseToInstallPrompt(response));
|
Logger.log(loc.userResponseToInstallPrompt(response));
|
||||||
}
|
}
|
||||||
@@ -340,6 +390,7 @@ async function promptToUpdateAzdata(newVersion: string, userRequested: boolean =
|
|||||||
? [loc.yes, loc.no]
|
? [loc.yes, loc.no]
|
||||||
: [loc.yes, loc.askLater, loc.doNotAskAgain];
|
: [loc.yes, loc.askLater, loc.doNotAskAgain];
|
||||||
if (config === AzdataDeployOption.prompt) {
|
if (config === AzdataDeployOption.prompt) {
|
||||||
|
Logger.log(loc.promptForAzdataUpdateLog(newVersion));
|
||||||
response = await vscode.window.showInformationMessage(loc.promptForAzdataUpdate(newVersion), ...responses);
|
response = await vscode.window.showInformationMessage(loc.promptForAzdataUpdate(newVersion), ...responses);
|
||||||
Logger.log(loc.userResponseToUpdatePrompt(response));
|
Logger.log(loc.userResponseToUpdatePrompt(response));
|
||||||
}
|
}
|
||||||
@@ -362,15 +413,26 @@ async function promptToUpdateAzdata(newVersion: string, userRequested: boolean =
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prompts user to accept EULA it if was not previously accepted. Stores and returns the user response to EULA prompt.
|
* Returns true if Eula has been accepted.
|
||||||
|
*
|
||||||
|
* @param memento The memento that stores the eulaAccepted state
|
||||||
|
*/
|
||||||
|
export function isEulaAccepted(memento: vscode.Memento): boolean {
|
||||||
|
return !!memento.get<boolean>(eulaAccepted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompts user to accept EULA. Stores and returns the user response to EULA prompt.
|
||||||
* @param memento - memento where the user response is stored.
|
* @param memento - memento where the user response is stored.
|
||||||
* @param userRequested - if true this operation was requested in response to a user issued command, if false it was issued at startup by system
|
* @param userRequested - if true this operation was requested in response to a user issued command, if false it was issued at startup by system
|
||||||
|
* @param requireUserAction - if the prompt is required to be acted upon by the user. This is typically 'true' when this method is called to address an Error when the EULA needs to be accepted to proceed.
|
||||||
* pre-requisite, the calling code has to ensure that the eula has not yet been previously accepted by the user.
|
* pre-requisite, the calling code has to ensure that the eula has not yet been previously accepted by the user.
|
||||||
* returns true if the user accepted the EULA.
|
* returns true if the user accepted the EULA.
|
||||||
*/
|
*/
|
||||||
|
export async function promptForEula(memento: vscode.Memento, userRequested: boolean = false, requireUserAction: boolean = false): Promise<boolean> {
|
||||||
export async function promptForEula(memento: vscode.Memento, userRequested: boolean = false): Promise<boolean> {
|
|
||||||
let response: string | undefined = loc.no;
|
let response: string | undefined = loc.no;
|
||||||
const config = <AzdataDeployOption>getConfig(azdataAcceptEulaKey);
|
const config = <AzdataDeployOption>getConfig(azdataAcceptEulaKey);
|
||||||
if (userRequested) {
|
if (userRequested) {
|
||||||
@@ -383,7 +445,9 @@ export async function promptForEula(memento: vscode.Memento, userRequested: bool
|
|||||||
if (config === AzdataDeployOption.prompt || userRequested) {
|
if (config === AzdataDeployOption.prompt || userRequested) {
|
||||||
Logger.show();
|
Logger.show();
|
||||||
Logger.log(loc.promptForEulaLog(microsoftPrivacyStatementUrl, eulaUrl));
|
Logger.log(loc.promptForEulaLog(microsoftPrivacyStatementUrl, eulaUrl));
|
||||||
response = await vscode.window.showInformationMessage(loc.promptForEula(microsoftPrivacyStatementUrl, eulaUrl), ...responses);
|
response = requireUserAction
|
||||||
|
? await vscode.window.showErrorMessage(loc.promptForEula(microsoftPrivacyStatementUrl, eulaUrl), ...responses)
|
||||||
|
: await vscode.window.showInformationMessage(loc.promptForEula(microsoftPrivacyStatementUrl, eulaUrl), ...responses);
|
||||||
Logger.log(loc.userResponseToEulaPrompt(response));
|
Logger.log(loc.userResponseToEulaPrompt(response));
|
||||||
}
|
}
|
||||||
if (response === loc.doNotAskAgain) {
|
if (response === loc.doNotAskAgain) {
|
||||||
@@ -400,9 +464,16 @@ export async function promptForEula(memento: vscode.Memento, userRequested: bool
|
|||||||
* Downloads the Windows installer and runs it
|
* Downloads the Windows installer and runs it
|
||||||
*/
|
*/
|
||||||
async function downloadAndInstallAzdataWin32(): Promise<void> {
|
async function downloadAndInstallAzdataWin32(): Promise<void> {
|
||||||
|
const downLoadLink = await getPlatformDownloadLink();
|
||||||
const downloadFolder = os.tmpdir();
|
const downloadFolder = os.tmpdir();
|
||||||
const downloadedFile = await HttpClient.downloadFile(`${azdataHostname}/${azdataUri}`, downloadFolder);
|
const downloadLogs = path.join(downloadFolder, 'ads_azdata_install_logs.log');
|
||||||
await executeCommand('msiexec', ['/qn', '/i', downloadedFile]);
|
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.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -475,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
|
// 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.
|
// 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:
|
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
|
* Parses out the azdata version from the raw azdata version output
|
||||||
* @param raw The raw version output from azdata --version
|
* @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));
|
Logger.log(loc.stdoutOutput(stdout));
|
||||||
}
|
}
|
||||||
if (stderr) {
|
if (stderr) {
|
||||||
Logger.log(loc.stdoutOutput(stderr));
|
Logger.log(loc.stderrOutput(stderr));
|
||||||
}
|
}
|
||||||
if (code) {
|
if (code) {
|
||||||
const err = new ExitCodeError(code, stderr);
|
const err = new ExitCodeError(code, stderr);
|
||||||
@@ -94,7 +94,7 @@ export async function executeSudoCommand(command: string): Promise<ProcessOutput
|
|||||||
Logger.log(loc.stdoutOutput(stdout));
|
Logger.log(loc.stdoutOutput(stdout));
|
||||||
}
|
}
|
||||||
if (stderr) {
|
if (stderr) {
|
||||||
Logger.log(loc.stdoutOutput(stderr));
|
Logger.log(loc.stderrOutput(stderr));
|
||||||
}
|
}
|
||||||
if (error) {
|
if (error) {
|
||||||
Logger.log(loc.unexpectedCommandError(error.message));
|
Logger.log(loc.unexpectedCommandError(error.message));
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export namespace HttpClient {
|
|||||||
if (targetFolder !== undefined) {
|
if (targetFolder !== undefined) {
|
||||||
const filename = path.basename(response.request.path);
|
const filename = path.basename(response.request.path);
|
||||||
const targetPath = path.join(targetFolder, filename);
|
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
|
// Wait to create the WriteStream until here so we can use the actual
|
||||||
// filename based off of the URI.
|
// filename based off of the URI.
|
||||||
downloadRequest.pipe(fs.createWriteStream(targetPath))
|
downloadRequest.pipe(fs.createWriteStream(targetPath))
|
||||||
@@ -73,6 +73,11 @@ export namespace HttpClient {
|
|||||||
reject(downloadError);
|
reject(downloadError);
|
||||||
downloadRequest.abort();
|
downloadRequest.abort();
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
response.on('end', () => {
|
||||||
|
Logger.log(loc.downloadFinished);
|
||||||
|
resolve(strings.join(''));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
let contentLength = response.headers['content-length'];
|
let contentLength = response.headers['content-length'];
|
||||||
let totalBytes = parseInt(contentLength || '0');
|
let totalBytes = parseInt(contentLength || '0');
|
||||||
@@ -92,13 +97,6 @@ export namespace HttpClient {
|
|||||||
printThreshold += 0.1;
|
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 vscode from 'vscode';
|
||||||
|
import * as loc from '../localizedConstants';
|
||||||
|
|
||||||
export class Log {
|
export class Log {
|
||||||
private _output: vscode.OutputChannel;
|
private _output: vscode.OutputChannel;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this._output = vscode.window.createOutputChannel('azdata');
|
this._output = vscode.window.createOutputChannel(loc.azdata);
|
||||||
}
|
}
|
||||||
|
|
||||||
log(msg: string): void {
|
log(msg: string): void {
|
||||||
this._output.appendLine(msg);
|
this._output.appendLine(`[${new Date().toISOString()}] ${msg}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
show(): void {
|
show(): void {
|
||||||
|
|||||||
@@ -3,8 +3,19 @@
|
|||||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import * as azdataExt from 'azdata-ext';
|
||||||
import * as which from 'which';
|
import * as which from 'which';
|
||||||
|
import * as loc from '../localizedConstants';
|
||||||
|
|
||||||
|
export class NoAzdataError extends Error implements azdataExt.ErrorWithLink {
|
||||||
|
constructor() {
|
||||||
|
super(loc.noAzdata);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get messageWithLink(): string {
|
||||||
|
return loc.noAzdataWithLink;
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Searches for the first instance of the specified executable in the PATH environment variable
|
* Searches for the first instance of the specified executable in the PATH environment variable
|
||||||
* @param exe The executable to search for
|
* @param exe The executable to search for
|
||||||
|
|||||||
@@ -4,135 +4,70 @@
|
|||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import * as azdataExt from 'azdata-ext';
|
import * as azdataExt from 'azdata-ext';
|
||||||
|
import * as rd from 'resource-deployment';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { checkAndInstallAzdata, checkAndUpdateAzdata, findAzdata, IAzdataTool, promptForEula } from './azdata';
|
import { getExtensionApi } from './api';
|
||||||
|
import { checkAndInstallAzdata, checkAndUpdateAzdata, findAzdata, isEulaAccepted, promptForEula } from './azdata';
|
||||||
import Logger from './common/logger';
|
import Logger from './common/logger';
|
||||||
import * as constants from './constants';
|
import * as constants from './constants';
|
||||||
import * as loc from './localizedConstants';
|
import * as loc from './localizedConstants';
|
||||||
|
import { ArcControllerConfigProfilesOptionsSource } from './providers/arcControllerConfigProfilesOptionsSource';
|
||||||
let localAzdata: IAzdataTool | undefined = undefined;
|
import { AzdataToolService } from './services/azdataToolService';
|
||||||
let eulaAccepted: boolean = false;
|
|
||||||
|
|
||||||
export async function activate(context: vscode.ExtensionContext): Promise<azdataExt.IExtension> {
|
export async function activate(context: vscode.ExtensionContext): Promise<azdataExt.IExtension> {
|
||||||
|
const azdataToolService = new AzdataToolService();
|
||||||
|
let eulaAccepted: boolean = false;
|
||||||
vscode.commands.registerCommand('azdata.acceptEula', async () => {
|
vscode.commands.registerCommand('azdata.acceptEula', async () => {
|
||||||
eulaAccepted = await promptForEula(context.globalState, true /* userRequested */);
|
await promptForEula(context.globalState, true /* userRequested */);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
vscode.commands.registerCommand('azdata.install', async () => {
|
vscode.commands.registerCommand('azdata.install', async () => {
|
||||||
localAzdata = await checkAndInstallAzdata(true /* userRequested */);
|
azdataToolService.localAzdata = await checkAndInstallAzdata(true /* userRequested */);
|
||||||
});
|
});
|
||||||
|
|
||||||
vscode.commands.registerCommand('azdata.update', async () => {
|
vscode.commands.registerCommand('azdata.update', async () => {
|
||||||
if (await checkAndUpdateAzdata(localAzdata, true /* userRequested */)) { // if an update was performed
|
if (await checkAndUpdateAzdata(azdataToolService.localAzdata, true /* userRequested */)) { // if an update was performed
|
||||||
localAzdata = await findAzdata(); // find and save the currently installed azdata
|
azdataToolService.localAzdata = await findAzdata(); // find and save the currently installed azdata
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
eulaAccepted = !!context.globalState.get<boolean>(constants.eulaAccepted); // fetch eula acceptance state from memento
|
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.
|
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));
|
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
|
// Don't block on this since we want the extension to finish activating without needing user input
|
||||||
checkAndInstallAzdata() // install if not installed and user wants it.
|
const localAzdataDiscovered = checkAndInstallAzdata() // install if not installed and user wants it.
|
||||||
.then(async azdataTool => {
|
.then(async azdataTool => {
|
||||||
localAzdata = azdataTool;
|
if (azdataTool !== undefined) {
|
||||||
if (localAzdata !== undefined) {
|
azdataToolService.localAzdata = azdataTool;
|
||||||
|
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));
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
//update if available and user wants it.
|
//update if available and user wants it.
|
||||||
if (await checkAndUpdateAzdata(localAzdata)) { // if an update was performed
|
if (await checkAndUpdateAzdata(azdataToolService.localAzdata)) { // if an update was performed
|
||||||
localAzdata = await findAzdata(); // find and save the currently installed azdata
|
azdataToolService.localAzdata = await findAzdata(); // find and save the currently installed azdata
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
vscode.window.showWarningMessage(loc.updateError(err));
|
vscode.window.showWarningMessage(loc.updateError(err));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return azdataTool;
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
const azdataApi = getExtensionApi(context.globalState, azdataToolService, localAzdataDiscovered);
|
||||||
azdata: {
|
|
||||||
arc: {
|
|
||||||
dc: {
|
|
||||||
create: async (namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string) => {
|
|
||||||
await throwIfNoAzdataOrEulaNotAccepted();
|
|
||||||
return localAzdata!.arc.dc.create(namespace, name, connectivityMode, resourceGroup, location, subscription, profileName, storageClass);
|
|
||||||
},
|
|
||||||
endpoint: {
|
|
||||||
list: async () => {
|
|
||||||
await throwIfNoAzdataOrEulaNotAccepted();
|
|
||||||
return localAzdata!.arc.dc.endpoint.list();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
config: {
|
|
||||||
list: async () => {
|
|
||||||
await throwIfNoAzdataOrEulaNotAccepted();
|
|
||||||
return localAzdata!.arc.dc.config.list();
|
|
||||||
},
|
|
||||||
show: async () => {
|
|
||||||
await throwIfNoAzdataOrEulaNotAccepted();
|
|
||||||
return localAzdata!.arc.dc.config.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
postgres: {
|
|
||||||
server: {
|
|
||||||
list: async () => {
|
|
||||||
await throwIfNoAzdataOrEulaNotAccepted();
|
|
||||||
return localAzdata!.arc.postgres.server.list();
|
|
||||||
},
|
|
||||||
show: async (name: string) => {
|
|
||||||
await throwIfNoAzdataOrEulaNotAccepted();
|
|
||||||
return localAzdata!.arc.postgres.server.show(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sql: {
|
|
||||||
mi: {
|
|
||||||
delete: async (name: string) => {
|
|
||||||
await throwIfNoAzdataOrEulaNotAccepted();
|
|
||||||
return localAzdata!.arc.sql.mi.delete(name);
|
|
||||||
},
|
|
||||||
list: async () => {
|
|
||||||
await throwIfNoAzdataOrEulaNotAccepted();
|
|
||||||
return localAzdata!.arc.sql.mi.list();
|
|
||||||
},
|
|
||||||
show: async (name: string) => {
|
|
||||||
await throwIfNoAzdataOrEulaNotAccepted();
|
|
||||||
return localAzdata!.arc.sql.mi.show(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
login: async (endpoint: string, username: string, password: string) => {
|
|
||||||
await throwIfNoAzdataOrEulaNotAccepted();
|
|
||||||
return localAzdata!.login(endpoint, username, password);
|
|
||||||
},
|
|
||||||
version: async () => {
|
|
||||||
await throwIfNoAzdataOrEulaNotAccepted();
|
|
||||||
return localAzdata!.version();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function throwIfNoAzdataOrEulaNotAccepted(): Promise<void> {
|
// register option source(s)
|
||||||
if (!localAzdata) {
|
const rdApi = <rd.IExtension>vscode.extensions.getExtension(rd.extension.name)?.exports;
|
||||||
Logger.log(loc.noAzdata);
|
rdApi.registerOptionsSourceProvider(new ArcControllerConfigProfilesOptionsSource(azdataApi));
|
||||||
throw new Error(loc.noAzdata);
|
|
||||||
}
|
return azdataApi;
|
||||||
if (!eulaAccepted) {
|
|
||||||
Logger.log(loc.eulaNotAccepted);
|
|
||||||
throw new Error(loc.eulaNotAccepted);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deactivate(): void { }
|
export function deactivate(): void { }
|
||||||
|
|||||||
@@ -4,16 +4,18 @@
|
|||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import * as nls from 'vscode-nls';
|
import * as nls from 'vscode-nls';
|
||||||
|
import { getErrorMessage } from './common/utils';
|
||||||
import { azdataConfigSection, azdataInstallKey, azdataUpdateKey } from './constants';
|
import { azdataConfigSection, azdataInstallKey, azdataUpdateKey } from './constants';
|
||||||
const localize = nls.loadMessageBundle();
|
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 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 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 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 downloadFinished = localize('azdata.downloadFinished', "Download finished");
|
||||||
export const installingAzdata = localize('azdata.installingAzdata', "Installing azdata...");
|
export const installingAzdata = localize('azdata.installingAzdata', "Installing Azure Data CLI...");
|
||||||
export const updatingAzdata = localize('azdata.updatingAzdata', "updating azdata...");
|
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 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 azdataUpdated = (version: string) => localize('azdata.azdataUpdated', "Azure Data CLI was successfully updated to version: {0}.", version);
|
||||||
export const yes = localize('azdata.yes', "Yes");
|
export const yes = localize('azdata.yes', "Yes");
|
||||||
@@ -22,45 +24,45 @@ export const accept = localize('azdata.accept', "Accept");
|
|||||||
export const decline = localize('azdata.decline', "Decline");
|
export const decline = localize('azdata.decline', "Decline");
|
||||||
export const doNotAskAgain = localize('azdata.doNotAskAgain', "Don't Ask Again");
|
export const doNotAskAgain = localize('azdata.doNotAskAgain', "Don't Ask Again");
|
||||||
export const askLater = localize('azdata.askLater', "Ask Later");
|
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 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 stdoutOutput = (stdout: string): string => localize('azdata.stdoutOutput', "stdout: {0}", stdout);
|
||||||
export const stderrOutput = (stderr: string): string => localize('azdata.stderrOutput', "stderr: {0}", stderr);
|
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 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 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 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 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 azdata: {0} is same or newer than any other version available", currentVersion);
|
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 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 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 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 promptForAzdataUpdateLog = (version: string): string => promptLog(promptForAzdataUpdate(version));
|
||||||
|
|
||||||
export const downloadError = localize('azdata.downloadError', "Error while downloading");
|
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 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 azdata: {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 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 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 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 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 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 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 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 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 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 azdata: {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 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 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 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 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, [accept the EULA](command:azdata.acceptEula) to use the features that require it.");
|
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 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 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 userResponseToEulaPrompt = (response: string | undefined) => localize('azdata.promptForEulaResponse', "User response to EULA prompt: {0}", response);
|
||||||
export const eulaAcceptedStateOnStartup = (eulaAccepted: boolean) => localize('azdata.eulaAcceptedStateOnStartup', "eulaAccepted state on startup: {0}", eulaAccepted);
|
export const eulaAcceptedStateOnStartup = (eulaAccepted: boolean) => localize('azdata.eulaAcceptedStateOnStartup', "'EULA Accepted' state on startup: {0}", eulaAccepted);
|
||||||
export const eulaAcceptedStateUpdated = (eulaAccepted: boolean) => localize('azdata.eulaAcceptedStateUpdated', "Updated 'eulaAccepted' state to: {0}", eulaAccepted);
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import * as rd from 'resource-deployment';
|
||||||
|
import * as azdataExt from 'azdata-ext';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class that provides options sources for an Arc Data Controller
|
||||||
|
*/
|
||||||
|
export class ArcControllerConfigProfilesOptionsSource implements rd.IOptionsSourceProvider {
|
||||||
|
readonly optionsSourceId = 'arc.controller.config.profiles';
|
||||||
|
constructor(private _azdataExtApi: azdataExt.IExtension) { }
|
||||||
|
async getOptions(): Promise<string[]> {
|
||||||
|
if (!this._azdataExtApi.isEulaAccepted()) { // if eula has not yet be accepted then give user a chance to accept it
|
||||||
|
await this._azdataExtApi.promptForEula();
|
||||||
|
}
|
||||||
|
return (await this._azdataExtApi.azdata.arc.dc.config.list()).result;
|
||||||
|
}
|
||||||
|
}
|
||||||
29
extensions/azdata/src/services/azdataToolService.ts
Normal file
29
extensions/azdata/src/services/azdataToolService.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import { IAzdataTool } from '../azdata';
|
||||||
|
|
||||||
|
export class AzdataToolService {
|
||||||
|
private _localAzdata: IAzdataTool | undefined;
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the localAzdata that was last saved
|
||||||
|
*/
|
||||||
|
get localAzdata(): IAzdataTool | undefined {
|
||||||
|
return this._localAzdata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the localAzdata that was last saved
|
||||||
|
*
|
||||||
|
* @param memento The memento that stores the localAzdata object
|
||||||
|
*/
|
||||||
|
set localAzdata(azdata: IAzdataTool | undefined) {
|
||||||
|
this._localAzdata = azdata;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -3,8 +3,6 @@
|
|||||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import * as path from 'path';
|
|
||||||
import { SemVer } from 'semver';
|
|
||||||
import * as should from 'should';
|
import * as should from 'should';
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
@@ -13,9 +11,13 @@ import * as childProcess from '../common/childProcess';
|
|||||||
import { HttpClient } from '../common/httpClient';
|
import { HttpClient } from '../common/httpClient';
|
||||||
import * as utils from '../common/utils';
|
import * as utils from '../common/utils';
|
||||||
import * as loc from '../localizedConstants';
|
import * as loc from '../localizedConstants';
|
||||||
import * as constants from '../constants';
|
|
||||||
|
|
||||||
const oldAzdataMock = <azdata.AzdataTool>{ path: '/path/to/azdata', cachedVersion: new SemVer('0.0.0') };
|
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 = {
|
const releaseJson = {
|
||||||
win32: {
|
win32: {
|
||||||
'version': '9999.999.999',
|
'version': '9999.999.999',
|
||||||
@@ -28,7 +30,7 @@ const releaseJson = {
|
|||||||
'version': '9999.999.999'
|
'version': '9999.999.999'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let executeSudoCommandStub: sinon.SinonStub;
|
||||||
|
|
||||||
describe('azdata', function () {
|
describe('azdata', function () {
|
||||||
afterEach(function (): void {
|
afterEach(function (): void {
|
||||||
@@ -56,9 +58,11 @@ describe('azdata', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('installAzdata', function (): void {
|
describe('installAzdata', function (): void {
|
||||||
|
|
||||||
beforeEach(function (): void {
|
beforeEach(function (): void {
|
||||||
sinon.stub(vscode.window, 'showErrorMessage').returns(Promise.resolve(<any>loc.yes));
|
sinon.stub(vscode.window, 'showErrorMessage').returns(Promise.resolve(<any>loc.yes));
|
||||||
sinon.stub(utils, 'searchForCmd').returns(Promise.resolve('/path/to/azdata'));
|
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> {
|
it('successful install', async function (): Promise<void> {
|
||||||
@@ -78,8 +82,12 @@ describe('azdata', function () {
|
|||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
it('unsuccessful download - win32', async function (): Promise<void> {
|
it('unsuccessful download - win32', async function (): Promise<void> {
|
||||||
sinon.stub(HttpClient, 'downloadFile').rejects();
|
sinon.stub(HttpClient, 'downloadFile').rejects();
|
||||||
const downloadPromise = azdata.checkAndInstallAzdata();
|
sinon.stub(childProcess, 'executeCommand')
|
||||||
await should(downloadPromise).be.rejected();
|
.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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +109,7 @@ describe('azdata', function () {
|
|||||||
describe('updateAzdata', function (): void {
|
describe('updateAzdata', function (): void {
|
||||||
beforeEach(function (): void {
|
beforeEach(function (): void {
|
||||||
sinon.stub(vscode.window, 'showInformationMessage').returns(Promise.resolve(<any>loc.yes));
|
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> {
|
it('successful update', async function (): Promise<void> {
|
||||||
@@ -108,7 +117,6 @@ describe('azdata', function () {
|
|||||||
case 'win32':
|
case 'win32':
|
||||||
await testWin32SuccessfulUpdate();
|
await testWin32SuccessfulUpdate();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'darwin':
|
case 'darwin':
|
||||||
await testDarwinSuccessfulUpdate();
|
await testDarwinSuccessfulUpdate();
|
||||||
break;
|
break;
|
||||||
@@ -133,9 +141,8 @@ describe('azdata', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('discoverLatestAvailableAzdataVersion', function (): void {
|
describe('discoverLatestAvailableAzdataVersion', function (): void {
|
||||||
this.timeout(20000);
|
it('finds latest available version of azdata successfully', async function (): Promise<void> {
|
||||||
it(`finds latest available version of azdata successfully`, async function (): Promise<void> {
|
sinon.stub(HttpClient, 'getTextContent').resolves(JSON.stringify(releaseJson));
|
||||||
// if the latest version is not discovered then the following call throws failing the test
|
|
||||||
await azdata.discoverLatestAvailableAzdataVersion();
|
await azdata.discoverLatestAvailableAzdataVersion();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -143,7 +150,7 @@ describe('azdata', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function testLinuxUnsuccessfulUpdate() {
|
async function testLinuxUnsuccessfulUpdate() {
|
||||||
const executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand').rejects();
|
executeSudoCommandStub.rejects();
|
||||||
const updateDone = await azdata.checkAndUpdateAzdata(oldAzdataMock);
|
const updateDone = await azdata.checkAndUpdateAzdata(oldAzdataMock);
|
||||||
should(updateDone).be.false();
|
should(updateDone).be.false();
|
||||||
should(executeSudoCommandStub.calledOnce).be.true();
|
should(executeSudoCommandStub.calledOnce).be.true();
|
||||||
@@ -173,7 +180,7 @@ async function testDarwinUnsuccessfulUpdate() {
|
|||||||
return Promise.reject(new Error('not Found'));
|
return Promise.reject(new Error('not Found'));
|
||||||
})
|
})
|
||||||
.callsFake(async (_command: string, _args: string[]) => { // by default return success
|
.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);
|
const updateDone = await azdata.checkAndUpdateAzdata(oldAzdataMock);
|
||||||
should(updateDone).be.false();
|
should(updateDone).be.false();
|
||||||
@@ -182,16 +189,16 @@ async function testDarwinUnsuccessfulUpdate() {
|
|||||||
|
|
||||||
async function testWin32UnsuccessfulUpdate() {
|
async function testWin32UnsuccessfulUpdate() {
|
||||||
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
|
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
|
||||||
const executeCommandStub = sinon.stub(childProcess, 'executeCommand').rejects();
|
executeSudoCommandStub.rejects();
|
||||||
const updateDone = await azdata.checkAndUpdateAzdata(oldAzdataMock);
|
const updateDone = await azdata.checkAndUpdateAzdata(oldAzdataMock);
|
||||||
should(updateDone).be.false();
|
should(updateDone).be.false('Update should not have been successful');
|
||||||
should(executeCommandStub.calledOnce).be.true();
|
should(executeSudoCommandStub.calledOnce).be.true();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testLinuxSuccessfulUpdate() {
|
async function testLinuxSuccessfulUpdate() {
|
||||||
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
|
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 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);
|
await azdata.checkAndUpdateAzdata(oldAzdataMock);
|
||||||
should(executeSudoCommandStub.callCount).be.equal(6);
|
should(executeSudoCommandStub.callCount).be.equal(6);
|
||||||
should(executeCommandStub.calledOnce).be.true();
|
should(executeCommandStub.calledOnce).be.true();
|
||||||
@@ -210,51 +217,39 @@ async function testDarwinSuccessfulUpdate() {
|
|||||||
}];
|
}];
|
||||||
const executeCommandStub = sinon.stub(childProcess, 'executeCommand')
|
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.
|
.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[]) => {
|
.resolves({
|
||||||
should(command).be.equal('brew');
|
stderr: '',
|
||||||
should(args).deepEqual(['info', 'azdata-cli', '--json']);
|
stdout: JSON.stringify(brewInfoOutput)
|
||||||
return Promise.resolve({
|
|
||||||
stderr: '',
|
|
||||||
stdout: JSON.stringify(brewInfoOutput)
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.callsFake(async (_command: string, _args: string[]) => { // return success on all other command executions
|
.resolves({ stdout: '0.0.0', stderr: '' });
|
||||||
return Promise.resolve({ stdout: '0.0.0', stderr: '' });
|
|
||||||
});
|
|
||||||
await azdata.checkAndUpdateAzdata(oldAzdataMock);
|
await azdata.checkAndUpdateAzdata(oldAzdataMock);
|
||||||
should(executeCommandStub.callCount).be.equal(6);
|
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() {
|
async function testWin32SuccessfulUpdate() {
|
||||||
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
|
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
|
||||||
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
|
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);
|
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() {
|
async function testWin32SuccessfulInstall() {
|
||||||
|
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
|
||||||
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
|
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
|
||||||
const executeCommandStub = sinon.stub(childProcess, 'executeCommand')
|
const executeCommandStub = sinon.stub(childProcess, 'executeCommand')
|
||||||
.onFirstCall()
|
.onFirstCall()
|
||||||
.callsFake(async (_command: string, _args: string[]) => {
|
.rejects(new Error('not Found')) // First call mock the tool not being found
|
||||||
return Promise.reject(new Error('not Found'));
|
.resolves({ stdout: '1.0.0', stderr: '' });
|
||||||
})
|
executeSudoCommandStub
|
||||||
.callsFake(async (command: string, args: string[]) => {
|
.returns({ stdout: '', stderr: '' });
|
||||||
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.checkAndInstallAzdata();
|
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() {
|
async function testDarwinSuccessfulInstall() {
|
||||||
@@ -273,23 +268,17 @@ async function testDarwinSuccessfulInstall() {
|
|||||||
async function testLinuxSuccessfulInstall() {
|
async function testLinuxSuccessfulInstall() {
|
||||||
const executeCommandStub = sinon.stub(childProcess, 'executeCommand')
|
const executeCommandStub = sinon.stub(childProcess, 'executeCommand')
|
||||||
.onFirstCall()
|
.onFirstCall()
|
||||||
.callsFake(async (_command: string, _args: string[]) => {
|
.rejects(new Error('not Found'))
|
||||||
return Promise.reject(new Error('not Found'));
|
.resolves({ stdout: '0.0.0', stderr: '' });
|
||||||
})
|
executeSudoCommandStub
|
||||||
.callsFake(async (_command: string, _args: string[]) => {
|
.resolves({ stdout: 'success', stderr: '' });
|
||||||
return Promise.resolve({ stdout: '0.0.0', stderr: '' });
|
|
||||||
});
|
|
||||||
const executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand')
|
|
||||||
.callsFake(async (_command: string ) => {
|
|
||||||
return Promise.resolve({ stdout: 'success', stderr: '' });
|
|
||||||
});
|
|
||||||
await azdata.checkAndInstallAzdata();
|
await azdata.checkAndInstallAzdata();
|
||||||
should(executeSudoCommandStub.callCount).be.equal(6);
|
should(executeSudoCommandStub.callCount).be.equal(6);
|
||||||
should(executeCommandStub.calledThrice).be.true();
|
should(executeCommandStub.calledThrice).be.true();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testLinuxUnsuccessfulInstall() {
|
async function testLinuxUnsuccessfulInstall() {
|
||||||
const executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand').rejects();
|
executeSudoCommandStub.rejects();
|
||||||
const downloadPromise = azdata.installAzdata();
|
const downloadPromise = azdata.installAzdata();
|
||||||
await should(downloadPromise).be.rejected();
|
await should(downloadPromise).be.rejected();
|
||||||
should(executeSudoCommandStub.calledOnce).be.true();
|
should(executeSudoCommandStub.calledOnce).be.true();
|
||||||
@@ -303,9 +292,9 @@ async function testDarwinUnsuccessfulInstall() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function testWin32UnsuccessfulInstall() {
|
async function testWin32UnsuccessfulInstall() {
|
||||||
const executeCommandStub = sinon.stub(childProcess, 'executeCommand').rejects();
|
executeSudoCommandStub.rejects();
|
||||||
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
|
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
|
||||||
const downloadPromise = azdata.installAzdata();
|
const downloadPromise = azdata.installAzdata();
|
||||||
await should(downloadPromise).be.rejected();
|
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 {
|
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')
|
nock('https://127.0.0.1')
|
||||||
.get('/arbitraryFile')
|
.get('/arbitraryFile')
|
||||||
.replyWithFile(200, __filename);
|
.replyWithFile(200, __filename);
|
||||||
const receivedContents = await HttpClient.getTextContent(`https://127.0.0.1/arbitraryFile`);
|
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> {
|
it('rejects on response error', async function (): Promise<void> {
|
||||||
|
|||||||
128
extensions/azdata/src/typings/azdata-ext.d.ts
vendored
128
extensions/azdata/src/typings/azdata-ext.d.ts
vendored
@@ -4,6 +4,8 @@
|
|||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
declare module 'azdata-ext' {
|
declare module 'azdata-ext' {
|
||||||
|
import { SemVer } from 'semver';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Covers defining what the azdata extension exports to other extensions
|
* Covers defining what the azdata extension exports to other extensions
|
||||||
*
|
*
|
||||||
@@ -14,6 +16,10 @@ declare module 'azdata-ext' {
|
|||||||
name = 'Microsoft.azdata'
|
name = 'Microsoft.azdata'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ErrorWithLink extends Error {
|
||||||
|
messageWithLink: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DcEndpointListResult {
|
export interface DcEndpointListResult {
|
||||||
description: string, // "Management Proxy"
|
description: string, // "Management Proxy"
|
||||||
endpoint: string, // "https://10.91.86.39:30777"
|
endpoint: string, // "https://10.91.86.39:30777"
|
||||||
@@ -80,7 +86,7 @@ declare module 'azdata-ext' {
|
|||||||
location: string, // "eastus2euap",
|
location: string, // "eastus2euap",
|
||||||
resourceGroup: string, // "my-rg",
|
resourceGroup: string, // "my-rg",
|
||||||
subscription: string, // "a5082b29-8c6e-4bc5-8ddd-8ef39dfebc39"
|
subscription: string, // "a5082b29-8c6e-4bc5-8ddd-8ef39dfebc39"
|
||||||
},
|
},
|
||||||
controller: {
|
controller: {
|
||||||
'enableBilling': string, // "True"
|
'enableBilling': string, // "True"
|
||||||
'logs.rotation.days': string, // "7"
|
'logs.rotation.days': string, // "7"
|
||||||
@@ -137,25 +143,13 @@ declare module 'azdata-ext' {
|
|||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
readyReplicas: string, // "1/1"
|
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"
|
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 {
|
export interface PostgresServerShowResult {
|
||||||
apiVersion: string, // "arcdata.microsoft.com/v1alpha1"
|
apiVersion: string, // "arcdata.microsoft.com/v1alpha1"
|
||||||
kind: string, // "postgresql-12"
|
kind: string, // "postgresql-12"
|
||||||
@@ -169,25 +163,56 @@ declare module 'azdata-ext' {
|
|||||||
uid: string, // "26d0f5bb-0c0b-4225-a6b5-5be2bf6feac0"
|
uid: string, // "26d0f5bb-0c0b-4225-a6b5-5be2bf6feac0"
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
backups: {
|
engine: {
|
||||||
deltaMinutes: number, // 3,
|
extensions: {
|
||||||
fullMinutes: number, // 10,
|
name: string // "citus"
|
||||||
tiers: [
|
}[],
|
||||||
{
|
settings: {
|
||||||
retention: {
|
default: { [key: string]: string } // { "max_connections": "101", "work_mem": "4MB" }
|
||||||
maximums: string[], // [ "6", "512MB" ],
|
}
|
||||||
minimums: string[], // [ "3" ]
|
},
|
||||||
|
scale: {
|
||||||
|
shards: number // 1
|
||||||
|
},
|
||||||
|
scheduling: {
|
||||||
|
default: {
|
||||||
|
resources: {
|
||||||
|
requests: {
|
||||||
|
cpu: string, // "1.5"
|
||||||
|
memory: string // "256Mi"
|
||||||
},
|
},
|
||||||
storage: {
|
limits: {
|
||||||
volumeSize: string, // "1Gi"
|
cpu: string, // "1.5"
|
||||||
|
memory: string // "256Mi"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
},
|
},
|
||||||
status: {
|
service: {
|
||||||
readyPods: string, // "1/1",
|
type: string, // "NodePort"
|
||||||
state: string // "Ready"
|
port: number // 5432
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
data: {
|
||||||
|
className: string, // "local-storage"
|
||||||
|
size: string // "5Gi"
|
||||||
|
},
|
||||||
|
logs: {
|
||||||
|
className: string, // "local-storage"
|
||||||
|
size: string // "5Gi"
|
||||||
|
},
|
||||||
|
backups: {
|
||||||
|
className: string, // "local-storage"
|
||||||
|
size: string // "5Gi"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
externalEndpoint: string, // "10.130.12.136:26630"
|
||||||
|
readyPods: string, // "1/1",
|
||||||
|
state: string, // "Ready"
|
||||||
|
logSearchDashboard: string, // https://127.0.0.1:30777/kibana/app/kibana#/discover?_a=(query:(language:kuery,query:'custom_resource_name:pg1'))
|
||||||
|
metricsDashboard: string, // https://127.0.0.1:30777/grafana/d/40q72HnGk/sql-managed-instance-metrics?var-hostname=pg1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,8 +238,25 @@ declare module 'azdata-ext' {
|
|||||||
},
|
},
|
||||||
postgres: {
|
postgres: {
|
||||||
server: {
|
server: {
|
||||||
|
delete(name: string): Promise<AzdataOutput<void>>,
|
||||||
list(): Promise<AzdataOutput<PostgresServerListResult[]>>,
|
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: {
|
sql: {
|
||||||
@@ -224,12 +266,34 @@ declare module 'azdata-ext' {
|
|||||||
show(name: string): Promise<AzdataOutput<SqlMiShowResult>>
|
show(name: string): Promise<AzdataOutput<SqlMiShowResult>>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
getPath(): Promise<string>,
|
||||||
login(endpoint: string, username: string, password: string): Promise<AzdataOutput<any>>,
|
login(endpoint: string, username: string, password: string): Promise<AzdataOutput<any>>,
|
||||||
|
/**
|
||||||
|
* The semVersion corresponding to this installation of azdata. version() method should have been run
|
||||||
|
* before fetching this value to ensure that correct value is returned. This is almost always correct unless
|
||||||
|
* Azdata has gotten reinstalled in the background after this IAzdataApi object was constructed.
|
||||||
|
*/
|
||||||
|
getSemVersion(): Promise<SemVer>,
|
||||||
version(): Promise<AzdataOutput<string>>
|
version(): Promise<AzdataOutput<string>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IExtension {
|
export interface IExtension {
|
||||||
azdata: IAzdataApi;
|
azdata: IAzdataApi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns true if AZDATA CLI EULA has been previously accepted by the user.
|
||||||
|
*/
|
||||||
|
isEulaAccepted(): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompts user to accept EULA. Stores and returns the user response to EULA prompt.
|
||||||
|
* @param requireUserAction - if the prompt is required to be acted upon by the user. This is typically 'true' when this method is called to address an Error when the EULA needs to be accepted to proceed.
|
||||||
|
*
|
||||||
|
* pre-requisite, the calling code has to ensure that the EULA has not yet been previously accepted by the user. The code can use @see isEulaAccepted() call to ascertain this.
|
||||||
|
* returns true if the user accepted the EULA.
|
||||||
|
*/
|
||||||
|
promptForEula(requireUserAction?: boolean): Promise<boolean>;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
extensions/azdata/src/typings/refs.d.ts
vendored
1
extensions/azdata/src/typings/refs.d.ts
vendored
@@ -6,3 +6,4 @@
|
|||||||
/// <reference path='../../../../src/sql/azdata.d.ts'/>
|
/// <reference path='../../../../src/sql/azdata.d.ts'/>
|
||||||
/// <reference path='../../../../src/sql/azdata.proposed.d.ts'/>
|
/// <reference path='../../../../src/sql/azdata.proposed.d.ts'/>
|
||||||
/// <reference path='../../../../src/vs/vscode.d.ts'/>
|
/// <reference path='../../../../src/vs/vscode.d.ts'/>
|
||||||
|
/// <reference path='../../../resource-deployment/src/typings/resource-deployment.d.ts'/>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export class AzureResourceSubscriptionFilterService implements IAzureResourceSub
|
|||||||
|
|
||||||
const filters: string[] = [];
|
const filters: string[] = [];
|
||||||
for (const accountId in selectedSubscriptionsCache) {
|
for (const accountId in selectedSubscriptionsCache) {
|
||||||
filters.push(...selectedSubscriptionsCache[accountId].map((subcription) => `${accountId}/${subcription.id}/${subcription.name}`));
|
filters.push(...selectedSubscriptionsCache[accountId].map((subscription) => `${accountId}/${subscription.id}/${subscription.name}`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,17 +3,16 @@
|
|||||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import * as azdata from 'azdata';
|
|
||||||
import * as nls from 'vscode-nls';
|
|
||||||
import { azureResource } from 'azureResource';
|
|
||||||
import { GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult } from 'azurecore';
|
|
||||||
import { isArray } from 'util';
|
|
||||||
import { AzureResourceGroupService } from './providers/resourceGroup/resourceGroupService';
|
|
||||||
import { TokenCredentials } from '@azure/ms-rest-js';
|
|
||||||
import { AppContext } from '../appContext';
|
|
||||||
import { IAzureResourceSubscriptionService } from './interfaces';
|
|
||||||
import { AzureResourceServiceNames } from './constants';
|
|
||||||
import { ResourceGraphClient } from '@azure/arm-resourcegraph';
|
import { ResourceGraphClient } from '@azure/arm-resourcegraph';
|
||||||
|
import { TokenCredentials } from '@azure/ms-rest-js';
|
||||||
|
import * as azdata from 'azdata';
|
||||||
|
import { GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult } from 'azurecore';
|
||||||
|
import { azureResource } from 'azureResource';
|
||||||
|
import * as nls from 'vscode-nls';
|
||||||
|
import { AppContext } from '../appContext';
|
||||||
|
import { AzureResourceServiceNames } from './constants';
|
||||||
|
import { IAzureResourceSubscriptionFilterService, IAzureResourceSubscriptionService } from './interfaces';
|
||||||
|
import { AzureResourceGroupService } from './providers/resourceGroup/resourceGroupService';
|
||||||
|
|
||||||
const localize = nls.loadMessageBundle();
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
@@ -106,7 +105,7 @@ export function equals(one: any, other: any): boolean {
|
|||||||
|
|
||||||
export async function getResourceGroups(appContext: AppContext, account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors: boolean = false): Promise<GetResourceGroupsResult> {
|
export async function getResourceGroups(appContext: AppContext, account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors: boolean = false): Promise<GetResourceGroupsResult> {
|
||||||
const result: GetResourceGroupsResult = { resourceGroups: [], errors: [] };
|
const result: GetResourceGroupsResult = { resourceGroups: [], errors: [] };
|
||||||
if (!account?.properties?.tenants || !isArray(account.properties.tenants) || !subscription) {
|
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants) || !subscription) {
|
||||||
const error = new Error(localize('azure.accounts.getResourceGroups.invalidParamsError', "Invalid account or subscription"));
|
const error = new Error(localize('azure.accounts.getResourceGroups.invalidParamsError', "Invalid account or subscription"));
|
||||||
if (!ignoreErrors) {
|
if (!ignoreErrors) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -146,7 +145,7 @@ export async function runResourceQuery<T extends azureResource.AzureGraphResourc
|
|||||||
ignoreErrors: boolean = false,
|
ignoreErrors: boolean = false,
|
||||||
query: string): Promise<ResourceQueryResult<T>> {
|
query: string): Promise<ResourceQueryResult<T>> {
|
||||||
const result: ResourceQueryResult<T> = { resources: [], errors: [] };
|
const result: ResourceQueryResult<T> = { resources: [], errors: [] };
|
||||||
if (!account?.properties?.tenants || !isArray(account.properties.tenants)) {
|
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
|
||||||
const error = new Error(localize('azure.accounts.runResourceQuery.errors.invalidAccount', "Invalid account"));
|
const error = new Error(localize('azure.accounts.runResourceQuery.errors.invalidAccount', "Invalid account"));
|
||||||
if (!ignoreErrors) {
|
if (!ignoreErrors) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -227,7 +226,7 @@ export async function runResourceQuery<T extends azureResource.AzureGraphResourc
|
|||||||
|
|
||||||
export async function getSubscriptions(appContext: AppContext, account?: azdata.Account, ignoreErrors: boolean = false): Promise<GetSubscriptionsResult> {
|
export async function getSubscriptions(appContext: AppContext, account?: azdata.Account, ignoreErrors: boolean = false): Promise<GetSubscriptionsResult> {
|
||||||
const result: GetSubscriptionsResult = { subscriptions: [], errors: [] };
|
const result: GetSubscriptionsResult = { subscriptions: [], errors: [] };
|
||||||
if (!account?.properties?.tenants || !isArray(account.properties.tenants)) {
|
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
|
||||||
const error = new Error(localize('azure.accounts.getSubscriptions.invalidParamsError', "Invalid account"));
|
const error = new Error(localize('azure.accounts.getSubscriptions.invalidParamsError', "Invalid account"));
|
||||||
if (!ignoreErrors) {
|
if (!ignoreErrors) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -258,3 +257,30 @@ export async function getSubscriptions(appContext: AppContext, account?: azdata.
|
|||||||
}));
|
}));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getSelectedSubscriptions(appContext: AppContext, account?: azdata.Account, ignoreErrors: boolean = false): Promise<GetSubscriptionsResult> {
|
||||||
|
const result: GetSubscriptionsResult = { subscriptions: [], errors: [] };
|
||||||
|
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
|
||||||
|
const error = new Error(localize('azure.accounts.getSelectedSubscriptions.invalidParamsError', "Invalid account"));
|
||||||
|
if (!ignoreErrors) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
result.errors.push(error);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscriptionFilterService = appContext.getService<IAzureResourceSubscriptionFilterService>(AzureResourceServiceNames.subscriptionFilterService);
|
||||||
|
try {
|
||||||
|
result.subscriptions.push(...await subscriptionFilterService.getSelectedSubscriptions(account));
|
||||||
|
} catch (err) {
|
||||||
|
const error = new Error(localize('azure.accounts.getSelectedSubscriptions.queryError', "Error fetching subscriptions for account {0} : {1}",
|
||||||
|
account.displayInfo.displayName,
|
||||||
|
err instanceof Error ? err.message : err));
|
||||||
|
console.warn(error);
|
||||||
|
if (!ignoreErrors) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
result.errors.push(error);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|||||||
2
extensions/azurecore/src/azurecore.d.ts
vendored
2
extensions/azurecore/src/azurecore.d.ts
vendored
@@ -63,7 +63,7 @@ declare module 'azurecore' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IExtension {
|
export interface IExtension {
|
||||||
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean): Thenable<GetSubscriptionsResult>;
|
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly?: boolean): Thenable<GetSubscriptionsResult>;
|
||||||
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Thenable<GetResourceGroupsResult>;
|
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Thenable<GetResourceGroupsResult>;
|
||||||
/**
|
/**
|
||||||
* Converts a region value (@see AzureRegion) into the localized Display Name
|
* Converts a region value (@see AzureRegion) into the localized Display Name
|
||||||
|
|||||||
@@ -147,7 +147,11 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean): Thenable<azurecore.GetSubscriptionsResult> { return azureResourceUtils.getSubscriptions(appContext, account, ignoreErrors); },
|
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly: boolean = false): Thenable<azurecore.GetSubscriptionsResult> {
|
||||||
|
return selectedOnly
|
||||||
|
? azureResourceUtils.getSelectedSubscriptions(appContext, account, ignoreErrors)
|
||||||
|
: azureResourceUtils.getSubscriptions(appContext, account, ignoreErrors);
|
||||||
|
},
|
||||||
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Thenable<azurecore.GetResourceGroupsResult> { return azureResourceUtils.getResourceGroups(appContext, account, subscription, ignoreErrors); },
|
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Thenable<azurecore.GetResourceGroupsResult> { return azureResourceUtils.getResourceGroups(appContext, account, subscription, ignoreErrors); },
|
||||||
provideResources(): azureResource.IAzureResourceProvider[] {
|
provideResources(): azureResource.IAzureResourceProvider[] {
|
||||||
const arcFeaturedEnabled = vscode.workspace.getConfiguration(constants.extensionConfigSectionName).get('enableArcFeatures');
|
const arcFeaturedEnabled = vscode.workspace.getConfiguration(constants.extensionConfigSectionName).get('enableArcFeatures');
|
||||||
|
|||||||
20
extensions/big-data-cluster/src/bdc.d.ts
vendored
20
extensions/big-data-cluster/src/bdc.d.ts
vendored
@@ -8,10 +8,30 @@ declare module 'bdc' {
|
|||||||
getClusterController(url: string, authType: AuthType, username?: string, password?: string): IClusterController;
|
getClusterController(url: string, authType: AuthType, username?: string, password?: string): IClusterController;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IEndpointModel {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
endpoint?: string;
|
||||||
|
protocol?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IHttpResponse {
|
||||||
|
method?: string;
|
||||||
|
url?: string;
|
||||||
|
statusCode?: number;
|
||||||
|
statusMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEndPointsResponse {
|
||||||
|
response: IHttpResponse;
|
||||||
|
endPoints: IEndpointModel[];
|
||||||
|
}
|
||||||
|
|
||||||
export type AuthType = 'integrated' | 'basic';
|
export type AuthType = 'integrated' | 'basic';
|
||||||
|
|
||||||
export interface IClusterController {
|
export interface IClusterController {
|
||||||
getClusterConfig(): Promise<any>;
|
getClusterConfig(): Promise<any>;
|
||||||
getKnoxUsername(clusterUsername: string): Promise<string>;
|
getKnoxUsername(clusterUsername: string): Promise<string>;
|
||||||
|
getEndPoints(promptConnect?: boolean): Promise<IEndPointsResponse>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
import localVarRequest = require('request');
|
import localVarRequest = require('request');
|
||||||
import http = require('http');
|
import http = require('http');
|
||||||
|
import * as bdc from 'bdc';
|
||||||
|
|
||||||
let defaultBasePath = 'https://localhost';
|
let defaultBasePath = 'https://localhost';
|
||||||
|
|
||||||
@@ -203,7 +204,7 @@ export class Dashboards {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EndpointModel {
|
export class EndpointModel implements bdc.IEndpointModel {
|
||||||
'name'?: string;
|
'name'?: string;
|
||||||
'description'?: string;
|
'description'?: string;
|
||||||
'endpoint'?: string;
|
'endpoint'?: string;
|
||||||
@@ -1167,7 +1168,7 @@ export class DefaultApi {
|
|||||||
* @param {*} [options] Override http request options.
|
* @param {*} [options] Override http request options.
|
||||||
*/
|
*/
|
||||||
public createMount (xRequestId: string, connection: string, remote: string, mount: string, credentials?: any, options: any = {}) : Promise<{ response: http.IncomingMessage; body: any; }> {
|
public createMount (xRequestId: string, connection: string, remote: string, mount: string, credentials?: any, options: any = {}) : Promise<{ response: http.IncomingMessage; body: any; }> {
|
||||||
const localVarPath = this.basePath + '/api/v1/storage/mounts';
|
const localVarPath = this.basePath + '/api/v1/bdc/services/hdfs/mounts';
|
||||||
let localVarQueryParameters: any = {};
|
let localVarQueryParameters: any = {};
|
||||||
let localVarHeaderParams: any = (<any>Object).assign({}, this.defaultHeaders);
|
let localVarHeaderParams: any = (<any>Object).assign({}, this.defaultHeaders);
|
||||||
let localVarFormParams: any = {};
|
let localVarFormParams: any = {};
|
||||||
@@ -1315,7 +1316,7 @@ export class DefaultApi {
|
|||||||
* @param {*} [options] Override http request options.
|
* @param {*} [options] Override http request options.
|
||||||
*/
|
*/
|
||||||
public deleteMount (xRequestId: string, connection: string, mount: string, options: any = {}) : Promise<{ response: http.IncomingMessage; body: any; }> {
|
public deleteMount (xRequestId: string, connection: string, mount: string, options: any = {}) : Promise<{ response: http.IncomingMessage; body: any; }> {
|
||||||
const localVarPath = this.basePath + '/api/v1/storage/mounts';
|
const localVarPath = this.basePath + '/api/v1/bdc/services/hdfs/mounts';
|
||||||
let localVarQueryParameters: any = {};
|
let localVarQueryParameters: any = {};
|
||||||
let localVarHeaderParams: any = (<any>Object).assign({}, this.defaultHeaders);
|
let localVarHeaderParams: any = (<any>Object).assign({}, this.defaultHeaders);
|
||||||
let localVarFormParams: any = {};
|
let localVarFormParams: any = {};
|
||||||
@@ -1599,7 +1600,7 @@ export class DefaultApi {
|
|||||||
* @param {*} [options] Override http request options.
|
* @param {*} [options] Override http request options.
|
||||||
*/
|
*/
|
||||||
public listMounts (xRequestId: string, connection: string, mount?: string, options: any = {}) : Promise<{ response: http.IncomingMessage; body: any; }> {
|
public listMounts (xRequestId: string, connection: string, mount?: string, options: any = {}) : Promise<{ response: http.IncomingMessage; body: any; }> {
|
||||||
const localVarPath = this.basePath + '/api/v1/storage/mounts';
|
const localVarPath = this.basePath + '/api/v1/bdc/services/hdfs/mounts';
|
||||||
let localVarQueryParameters: any = {};
|
let localVarQueryParameters: any = {};
|
||||||
let localVarHeaderParams: any = (<any>Object).assign({}, this.defaultHeaders);
|
let localVarHeaderParams: any = (<any>Object).assign({}, this.defaultHeaders);
|
||||||
let localVarFormParams: any = {};
|
let localVarFormParams: any = {};
|
||||||
@@ -1668,7 +1669,7 @@ export class DefaultApi {
|
|||||||
* @param {*} [options] Override http request options.
|
* @param {*} [options] Override http request options.
|
||||||
*/
|
*/
|
||||||
public refreshMount (xRequestId: string, connection: string, mount: string, options: any = {}) : Promise<{ response: http.IncomingMessage; body: any; }> {
|
public refreshMount (xRequestId: string, connection: string, mount: string, options: any = {}) : Promise<{ response: http.IncomingMessage; body: any; }> {
|
||||||
const localVarPath = this.basePath + '/api/v1/storage/mounts/refresh';
|
const localVarPath = this.basePath + '/api/v1/bdc/services/hdfs/mounts/refresh';
|
||||||
let localVarQueryParameters: any = {};
|
let localVarQueryParameters: any = {};
|
||||||
let localVarHeaderParams: any = (<any>Object).assign({}, this.defaultHeaders);
|
let localVarHeaderParams: any = (<any>Object).assign({}, this.defaultHeaders);
|
||||||
let localVarFormParams: any = {};
|
let localVarFormParams: any = {};
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { TokenRouterApi } from './clusterApiGenerated2';
|
|||||||
import * as nls from 'vscode-nls';
|
import * as nls from 'vscode-nls';
|
||||||
import { ConnectControllerDialog, ConnectControllerModel } from '../dialog/connectControllerDialog';
|
import { ConnectControllerDialog, ConnectControllerModel } from '../dialog/connectControllerDialog';
|
||||||
import { getIgnoreSslVerificationConfigSetting } from '../utils';
|
import { getIgnoreSslVerificationConfigSetting } from '../utils';
|
||||||
import { IClusterController, AuthType } from 'bdc';
|
import { IClusterController, AuthType, IEndPointsResponse, IHttpResponse } from 'bdc';
|
||||||
|
|
||||||
const localize = nls.loadMessageBundle();
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
@@ -174,24 +174,17 @@ export class ClusterController implements IClusterController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getKnoxUsername(sqlLogin: string): Promise<string> {
|
public async getKnoxUsername(sqlLogin: string): Promise<string> {
|
||||||
try {
|
// This all is necessary because prior to CU5 BDC deployments all had the same default username for
|
||||||
// This all is necessary because prior to CU5 BDC deployments all had the same default username for
|
// accessing the Knox gateway. But in the allowRunAsRoot setting was added and defaulted to false - so
|
||||||
// accessing the Knox gateway. But in the allowRunAsRoot setting was added and defaulted to false - so
|
// if that exists and is false then we use the username instead.
|
||||||
// if that exists and is false then we use the username instead.
|
// Note that the SQL username may not necessarily be correct here either - but currently this is what
|
||||||
// Note that the SQL username may not necessarily be correct here either - but currently this is what
|
// we're requiring to run Notebooks in a BDC
|
||||||
// we're requiring to run Notebooks in a BDC
|
const config = await this.getClusterConfig();
|
||||||
const config = await this.getClusterConfig();
|
return config.spec?.spec?.security?.allowRunAsRoot === false ? sqlLogin : DEFAULT_KNOX_USERNAME;
|
||||||
return config.spec?.spec?.security?.allowRunAsRoot === false ? sqlLogin : DEFAULT_KNOX_USERNAME;
|
|
||||||
} catch (err) {
|
|
||||||
console.log(`Unexpected error fetching cluster config for getKnoxUsername ${err}`);
|
|
||||||
// Optimistically fall back to SQL login since root shouldn't be typically used going forward
|
|
||||||
return sqlLogin;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getClusterConfig(promptConnect: boolean = false): Promise<any> {
|
public async getClusterConfig(promptConnect: boolean = false): Promise<any> {
|
||||||
return await this.withConnectRetry<IEndPointsResponse>(
|
return await this.withConnectRetry<any>(
|
||||||
this.getClusterConfigImpl,
|
this.getClusterConfigImpl,
|
||||||
promptConnect,
|
promptConnect,
|
||||||
localize('bdc.error.getClusterConfig', "Error retrieving cluster config from {0}", this._url));
|
localize('bdc.error.getClusterConfig', "Error retrieving cluster config from {0}", this._url));
|
||||||
@@ -387,11 +380,6 @@ export interface IClusterRequest {
|
|||||||
method?: string;
|
method?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IEndPointsResponse {
|
|
||||||
response: IHttpResponse;
|
|
||||||
endPoints: EndpointModel[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IBdcStatusResponse {
|
export interface IBdcStatusResponse {
|
||||||
response: IHttpResponse;
|
response: IHttpResponse;
|
||||||
bdcStatus: BdcStatusModel;
|
bdcStatus: BdcStatusModel;
|
||||||
@@ -419,13 +407,6 @@ export interface MountStatusResponse {
|
|||||||
mount: MountInfo[];
|
mount: MountInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IHttpResponse {
|
|
||||||
method?: string;
|
|
||||||
url?: string;
|
|
||||||
statusCode?: number;
|
|
||||||
statusMessage?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ControllerError extends Error {
|
export class ControllerError extends Error {
|
||||||
public code?: number;
|
public code?: number;
|
||||||
public reason?: string;
|
public reason?: string;
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export class BdcDashboard extends InitializingComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async createDashboard(): Promise<void> {
|
private async createDashboard(): Promise<void> {
|
||||||
this.dashboard = azdata.window.createModelViewDashboard(this.title, { alwaysShowTabs: true });
|
this.dashboard = azdata.window.createModelViewDashboard(this.title, 'BdcDashboard', { alwaysShowTabs: true });
|
||||||
this.dashboard.registerTabs(async (modelView: azdata.ModelView) => {
|
this.dashboard.registerTabs(async (modelView: azdata.ModelView) => {
|
||||||
this.modelView = modelView;
|
this.modelView = modelView;
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import * as azdata from 'azdata';
|
import * as azdata from 'azdata';
|
||||||
import { ClusterController, ControllerError, IEndPointsResponse } from '../controller/clusterControllerApi';
|
import { ClusterController, ControllerError } from '../controller/clusterControllerApi';
|
||||||
import { Deferred } from '../../common/promise';
|
import { Deferred } from '../../common/promise';
|
||||||
import * as loc from '../localizedConstants';
|
import * as loc from '../localizedConstants';
|
||||||
import { AuthType } from 'bdc';
|
import { AuthType, IEndPointsResponse } from 'bdc';
|
||||||
|
|
||||||
function getAuthCategory(name: AuthType): azdata.CategoryValue {
|
function getAuthCategory(name: AuthType): azdata.CategoryValue {
|
||||||
if (name === 'basic') {
|
if (name === 'basic') {
|
||||||
|
|||||||
@@ -278,6 +278,7 @@ export function createViewContext(): ViewTestContext {
|
|||||||
validate: undefined!,
|
validate: undefined!,
|
||||||
initializeModel: () => { return Promise.resolve(); },
|
initializeModel: () => { return Promise.resolve(); },
|
||||||
modelBuilder: {
|
modelBuilder: {
|
||||||
|
listView: undefined!,
|
||||||
radioCardGroup: undefined!,
|
radioCardGroup: undefined!,
|
||||||
navContainer: undefined!,
|
navContainer: undefined!,
|
||||||
divContainer: () => divBuilder,
|
divContainer: () => divBuilder,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"downloadUrl": "https://sqlopsextensions.blob.core.windows.net/extensions/import/service/{#version#}/{#fileName#}",
|
"downloadUrl": "https://sqlopsextensions.blob.core.windows.net/extensions/import/service/{#version#}/{#fileName#}",
|
||||||
"useDefaultLinuxRuntime": true,
|
"useDefaultLinuxRuntime": true,
|
||||||
"version": "0.0.5",
|
"version": "0.0.6",
|
||||||
"downloadFileNames": {
|
"downloadFileNames": {
|
||||||
"Windows_64": "win-x64.zip",
|
"Windows_64": "win-x64.zip",
|
||||||
"Windows_86": "win-x86.zip",
|
"Windows_86": "win-x86.zip",
|
||||||
|
|||||||
@@ -96,10 +96,10 @@ export class ModifyColumnsPage extends ImportPage {
|
|||||||
this.model.proseColumns = [];
|
this.model.proseColumns = [];
|
||||||
this.table.data.forEach((row) => {
|
this.table.data.forEach((row) => {
|
||||||
this.model.proseColumns.push({
|
this.model.proseColumns.push({
|
||||||
columnName: row[0],
|
columnName: row[0].value,
|
||||||
dataType: row[1],
|
dataType: row[1].value,
|
||||||
primaryKey: row[2],
|
primaryKey: row[2].value,
|
||||||
nullable: row[3]
|
nullable: row[3].value
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import * as azdata from 'azdata';
|
|||||||
import { ImportPage } from '../api/importPage';
|
import { ImportPage } from '../api/importPage';
|
||||||
import { InsertDataResponse } from '../../services/contracts';
|
import { InsertDataResponse } from '../../services/contracts';
|
||||||
import * as constants from '../../common/constants';
|
import * as constants from '../../common/constants';
|
||||||
|
import { EOL } from 'os';
|
||||||
|
|
||||||
export class SummaryPage extends ImportPage {
|
export class SummaryPage extends ImportPage {
|
||||||
private _table: azdata.TableComponent;
|
private _table: azdata.TableComponent;
|
||||||
@@ -93,7 +94,7 @@ export class SummaryPage extends ImportPage {
|
|||||||
private populateTable() {
|
private populateTable() {
|
||||||
this.table.updateProperties({
|
this.table.updateProperties({
|
||||||
data: [
|
data: [
|
||||||
[constants.serverNameText, this.model.server.providerName],
|
[constants.serverNameText, this.model.server.options.server],
|
||||||
[constants.databaseText, this.model.database],
|
[constants.databaseText, this.model.database],
|
||||||
[constants.tableNameText, this.model.table],
|
[constants.tableNameText, this.model.table],
|
||||||
[constants.tableSchemaText, this.model.schema],
|
[constants.tableSchemaText, this.model.schema],
|
||||||
@@ -104,22 +105,36 @@ export class SummaryPage extends ImportPage {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleImport(): Promise<boolean> {
|
private async handleImport(): Promise<void> {
|
||||||
let changeColumnResults = [];
|
let i = 0;
|
||||||
this.model.proseColumns.forEach((val, i, arr) => {
|
const changeColumnSettingsErrors = [];
|
||||||
|
for (let val of this.model.proseColumns) {
|
||||||
let columnChangeParams = {
|
let columnChangeParams = {
|
||||||
index: i,
|
index: i++,
|
||||||
newName: val.columnName,
|
newName: val.columnName,
|
||||||
newDataType: val.dataType,
|
newDataType: val.dataType,
|
||||||
newNullable: val.nullable,
|
newNullable: val.nullable,
|
||||||
newInPrimaryKey: val.primaryKey
|
newInPrimaryKey: val.primaryKey
|
||||||
};
|
};
|
||||||
changeColumnResults.push(this.provider.sendChangeColumnSettingsRequest(columnChangeParams));
|
const changeColumnResult = await this.provider.sendChangeColumnSettingsRequest(columnChangeParams);
|
||||||
});
|
if (changeColumnResult?.result?.errorMessage) {
|
||||||
|
changeColumnSettingsErrors.push(changeColumnResult.result.errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stopping import if there are errors in change column setting.
|
||||||
|
if (changeColumnSettingsErrors.length !== 0) {
|
||||||
|
let updateText: string;
|
||||||
|
updateText = changeColumnSettingsErrors.join(EOL);
|
||||||
|
this.statusText.updateProperties({
|
||||||
|
value: updateText
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let result: InsertDataResponse;
|
let result: InsertDataResponse;
|
||||||
let err;
|
let err;
|
||||||
let includePasswordInConnectionString = (this.model.server.options.connectionId === 'Integrated') ? false : true;
|
let includePasswordInConnectionString = (this.model.server.options.authenticationType === 'Integrated') ? false : true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
result = await this.provider.sendInsertDataRequest({
|
result = await this.provider.sendInsertDataRequest({
|
||||||
@@ -151,7 +166,6 @@ export class SummaryPage extends ImportPage {
|
|||||||
this.statusText.updateProperties({
|
this.statusText.updateProperties({
|
||||||
value: updateText
|
value: updateText
|
||||||
});
|
});
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// private async getCountRowsInserted(): Promise<Number> {
|
// private async getCountRowsInserted(): Promise<Number> {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ suite('Dacpac integration test suite', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const bacpac1: string = path.join(__dirname, '..', '..', 'testData', 'Database1.bacpac');
|
const bacpac1: string = path.join(__dirname, '..', '..', 'testData', 'Database1.bacpac');
|
||||||
test('Import and export bacpac', async function () {
|
test('Import and export bacpac @UNSTABLE@', async function () {
|
||||||
const server = await getStandaloneServer();
|
const server = await getStandaloneServer();
|
||||||
await utils.connectToServer(server);
|
await utils.connectToServer(server);
|
||||||
|
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ export function invalidImportTableSchemaError(databaseName: string | undefined,
|
|||||||
export const loadModelParameterFailedError = localize('models.loadModelParameterFailedError', "Failed to load model parameters'");
|
export const loadModelParameterFailedError = localize('models.loadModelParameterFailedError', "Failed to load model parameters'");
|
||||||
export const unsupportedModelParameterType = localize('models.unsupportedModelParameterType', "unsupported");
|
export const unsupportedModelParameterType = localize('models.unsupportedModelParameterType', "unsupported");
|
||||||
export const dashboardTitle = localize('dashboardTitle', "Machine Learning");
|
export const dashboardTitle = localize('dashboardTitle', "Machine Learning");
|
||||||
export const dashboardDesc = localize('dashboardDesc', "Machine Learning for SQL Databases");
|
export const dashboardDesc = localize('dashboardDesc', "Machine Learning for SQL databases");
|
||||||
export const dashboardLinksTitle = localize('dashboardLinksTitle', "Useful links");
|
export const dashboardLinksTitle = localize('dashboardLinksTitle', "Useful links");
|
||||||
export const dashboardVideoLinksTitle = localize('dashboardVideoLinksTitle', "Video tutorials");
|
export const dashboardVideoLinksTitle = localize('dashboardVideoLinksTitle', "Video tutorials");
|
||||||
export const showMoreTitle = localize('showMoreTitle', "Show more");
|
export const showMoreTitle = localize('showMoreTitle', "Show more");
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export class AzurecoreApiStub implements azurecore.IExtension {
|
|||||||
runGraphQuery<T extends azureResource.AzureGraphResource>(_account: azdata.Account, _subscriptions: azureResource.AzureResourceSubscription[], _ignoreErrors: boolean, _query: string): Promise<azurecore.ResourceQueryResult<T>> {
|
runGraphQuery<T extends azureResource.AzureGraphResource>(_account: azdata.Account, _subscriptions: azureResource.AzureResourceSubscription[], _ignoreErrors: boolean, _query: string): Promise<azurecore.ResourceQueryResult<T>> {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
getSubscriptions(_account?: azdata.Account | undefined, _ignoreErrors?: boolean | undefined): Thenable<azurecore.GetSubscriptionsResult> {
|
getSubscriptions(_account?: azdata.Account | undefined, _ignoreErrors?: boolean | undefined, _selectedOnly?: boolean | undefined): Thenable<azurecore.GetSubscriptionsResult> {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
getResourceGroups(_account?: azdata.Account | undefined, _subscription?: azureResource.AzureResourceSubscription | undefined, _ignoreErrors?: boolean | undefined): Thenable<azurecore.GetResourceGroupsResult> {
|
getResourceGroups(_account?: azdata.Account | undefined, _subscription?: azureResource.AzureResourceSubscription | undefined, _ignoreErrors?: boolean | undefined): Thenable<azurecore.GetResourceGroupsResult> {
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ export function createViewContext(): ViewTestContext {
|
|||||||
validate: undefined!,
|
validate: undefined!,
|
||||||
initializeModel: () => { return Promise.resolve(); },
|
initializeModel: () => { return Promise.resolve(); },
|
||||||
modelBuilder: {
|
modelBuilder: {
|
||||||
|
listView: undefined!,
|
||||||
radioCardGroup: undefined!,
|
radioCardGroup: undefined!,
|
||||||
navContainer: undefined!,
|
navContainer: undefined!,
|
||||||
divContainer: () => divBuilder,
|
divContainer: () => divBuilder,
|
||||||
|
|||||||
@@ -186,13 +186,13 @@ export class CurrentModelsTable extends ModelViewBase implements IDataComponent<
|
|||||||
|
|
||||||
public async onLoading(): Promise<void> {
|
public async onLoading(): Promise<void> {
|
||||||
if (this._loader) {
|
if (this._loader) {
|
||||||
await this._loader.updateProperties({ loading: true });
|
this._loader.loading = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async onLoaded(): Promise<void> {
|
public async onLoaded(): Promise<void> {
|
||||||
if (this._loader) {
|
if (this._loader) {
|
||||||
await this._loader.updateProperties({ loading: false });
|
this._loader.loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,11 @@ export class ModelImportLocationPage extends ModelViewBase implements IPageView,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.importTable && this._labelComponent) {
|
if (this.importTable && this._labelComponent) {
|
||||||
|
|
||||||
|
// Add table name to the models imported.
|
||||||
|
// Since Table name is picked last as per new flow this hasn't been set yet.
|
||||||
|
this.modelsViewData?.forEach(x => x.targetImportTable = this.importTable);
|
||||||
|
|
||||||
if (!this.validateImportTableName()) {
|
if (!this.validateImportTableName()) {
|
||||||
this._labelComponent.value = constants.selectModelsTableMessage;
|
this._labelComponent.value = constants.selectModelsTableMessage;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ interface IActionMetadata {
|
|||||||
description?: string,
|
description?: string,
|
||||||
link?: string,
|
link?: string,
|
||||||
iconPath?: { light: string | vscode.Uri; dark: string | vscode.Uri },
|
iconPath?: { light: string | vscode.Uri; dark: string | vscode.Uri },
|
||||||
command?: string;
|
command?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxWidth = 800;
|
const maxWidth = 810;
|
||||||
const headerMaxHeight = 300;
|
const headerMaxHeight = 234;
|
||||||
export class DashboardWidget {
|
export class DashboardWidget {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,33 +33,30 @@ export class DashboardWidget {
|
|||||||
this._apiWrapper.registerWidget('mls.dashboard', async (view) => {
|
this._apiWrapper.registerWidget('mls.dashboard', async (view) => {
|
||||||
const container = view.modelBuilder.flexContainer().withLayout({
|
const container = view.modelBuilder.flexContainer().withLayout({
|
||||||
flexFlow: 'column',
|
flexFlow: 'column',
|
||||||
width: '100%',
|
width: 'auto',
|
||||||
height: '100%'
|
height: '100%'
|
||||||
}).component();
|
}).component();
|
||||||
const header = this.createHeader(view);
|
const header = await this.createHeader(view);
|
||||||
const tasksContainer = await this.createTasks(view);
|
|
||||||
const footerContainer = this.createFooter(view);
|
const footerContainer = this.createFooter(view);
|
||||||
container.addItem(header, {
|
container.addItem(header, {
|
||||||
CSSStyles: {
|
CSSStyles: {
|
||||||
'background-image': `url(${vscode.Uri.file(this.asAbsolutePath('images/background.svg'))})`,
|
'background-image': `
|
||||||
|
url(${vscode.Uri.file(this.asAbsolutePath('images/background.svg'))}),
|
||||||
|
linear-gradient(0deg, rgba(0,0,0,0.09) 0%, rgba(0,0,0,0) 100%)
|
||||||
|
`,
|
||||||
'background-repeat': 'no-repeat',
|
'background-repeat': 'no-repeat',
|
||||||
'background-position': 'bottom',
|
'background-position': 'left 32px',
|
||||||
|
'background-size': '107%',
|
||||||
|
'border': 'none',
|
||||||
'width': `${maxWidth}px`,
|
'width': `${maxWidth}px`,
|
||||||
'height': '330px',
|
'height': `${headerMaxHeight}px`
|
||||||
'background-size': `${maxWidth}px ${headerMaxHeight}px`,
|
|
||||||
'margin-bottom': '-60px'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
container.addItem(tasksContainer, {
|
|
||||||
CSSStyles: {
|
|
||||||
'width': `${maxWidth}px`,
|
|
||||||
'height': '150px',
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
container.addItem(footerContainer, {
|
container.addItem(footerContainer, {
|
||||||
CSSStyles: {
|
CSSStyles: {
|
||||||
'width': `${maxWidth}px`,
|
|
||||||
'height': '500px',
|
'height': '500px',
|
||||||
|
'width': `${maxWidth}px`,
|
||||||
|
'margin-top': '16px'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const mainContainer = view.modelBuilder.flexContainer()
|
const mainContainer = view.modelBuilder.flexContainer()
|
||||||
@@ -70,7 +67,7 @@ export class DashboardWidget {
|
|||||||
position: 'absolute'
|
position: 'absolute'
|
||||||
}).component();
|
}).component();
|
||||||
mainContainer.addItem(container, {
|
mainContainer.addItem(container, {
|
||||||
CSSStyles: { 'padding-top': '25px', 'padding-left': '5px' }
|
CSSStyles: { 'padding-top': '12px' }
|
||||||
});
|
});
|
||||||
await view.initializeModel(mainContainer);
|
await view.initializeModel(mainContainer);
|
||||||
resolve();
|
resolve();
|
||||||
@@ -78,7 +75,7 @@ export class DashboardWidget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private createHeader(view: azdata.ModelView): azdata.Component {
|
private async createHeader(view: azdata.ModelView): Promise<azdata.Component> {
|
||||||
const header = view.modelBuilder.flexContainer().withLayout({
|
const header = view.modelBuilder.flexContainer().withLayout({
|
||||||
flexFlow: 'column',
|
flexFlow: 'column',
|
||||||
width: maxWidth,
|
width: maxWidth,
|
||||||
@@ -88,7 +85,8 @@ export class DashboardWidget {
|
|||||||
value: constants.dashboardTitle,
|
value: constants.dashboardTitle,
|
||||||
CSSStyles: {
|
CSSStyles: {
|
||||||
'font-size': '36px',
|
'font-size': '36px',
|
||||||
'font-weight': 'bold',
|
'font-weight': '300',
|
||||||
|
'line-height': '48px',
|
||||||
'margin': '0px'
|
'margin': '0px'
|
||||||
}
|
}
|
||||||
}).component();
|
}).component();
|
||||||
@@ -96,14 +94,22 @@ export class DashboardWidget {
|
|||||||
value: constants.dashboardDesc,
|
value: constants.dashboardDesc,
|
||||||
CSSStyles: {
|
CSSStyles: {
|
||||||
'font-size': '14px',
|
'font-size': '14px',
|
||||||
'font-weight': 'bold',
|
'font-weight': '300',
|
||||||
|
'line-height': '20px',
|
||||||
'margin': '0px'
|
'margin': '0px'
|
||||||
}
|
}
|
||||||
}).component();
|
}).component();
|
||||||
header.addItems([titleComponent, descComponent], {
|
header.addItems([titleComponent, descComponent], {
|
||||||
CSSStyles: {
|
CSSStyles: {
|
||||||
'width': `${maxWidth}px`,
|
'padding-left': '26px'
|
||||||
'padding': '5px'
|
}
|
||||||
|
});
|
||||||
|
const tasksContainer = this.createTasks(view);
|
||||||
|
header.addItem(await tasksContainer, {
|
||||||
|
CSSStyles: {
|
||||||
|
'height': 'auto',
|
||||||
|
'margin-top': '67px',
|
||||||
|
'width': `${maxWidth}px`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -134,7 +140,6 @@ export class DashboardWidget {
|
|||||||
const videosContainer = view.modelBuilder.flexContainer().withLayout({
|
const videosContainer = view.modelBuilder.flexContainer().withLayout({
|
||||||
flexFlow: 'row',
|
flexFlow: 'row',
|
||||||
width: maxWidth,
|
width: maxWidth,
|
||||||
height: '300px',
|
|
||||||
}).component();
|
}).component();
|
||||||
|
|
||||||
links.forEach(link => {
|
links.forEach(link => {
|
||||||
@@ -163,10 +168,7 @@ export class DashboardWidget {
|
|||||||
}
|
}
|
||||||
}).component();
|
}).component();
|
||||||
const viewPanelStyle = {
|
const viewPanelStyle = {
|
||||||
'padding': '0px',
|
'padding': '10px 5px 10px 0',
|
||||||
'padding-right': '5px',
|
|
||||||
'padding-top': '20px',
|
|
||||||
'height': '200px',
|
|
||||||
'margin': '0px'
|
'margin': '0px'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -204,15 +206,14 @@ export class DashboardWidget {
|
|||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.addShowMorePanel(view, linksContainer, moreVideosContainer, { 'padding-left': '5px' }, viewPanelStyle);
|
this.addShowMorePanel(view, linksContainer, moreVideosContainer, { 'padding-top': '10px' }, viewPanelStyle);
|
||||||
return linksContainer;
|
return linksContainer;
|
||||||
}
|
}
|
||||||
|
|
||||||
private addShowMorePanel(view: azdata.ModelView, parentPanel: azdata.FlexContainer, morePanel: azdata.Component, moreButtonStyle: { [key: string]: string }, morePanelStyle: { [key: string]: string }): azdata.Component {
|
private addShowMorePanel(view: azdata.ModelView, parentPanel: azdata.FlexContainer, morePanel: azdata.Component, moreButtonStyle: { [key: string]: string }, morePanelStyle: { [key: string]: string }): azdata.Component {
|
||||||
const maxWidth = 100;
|
|
||||||
const linkContainer = view.modelBuilder.flexContainer().withLayout({
|
const linkContainer = view.modelBuilder.flexContainer().withLayout({
|
||||||
flexFlow: 'row',
|
flexFlow: 'row',
|
||||||
width: maxWidth + 10,
|
width: 'auto',
|
||||||
justifyContent: 'flex-start'
|
justifyContent: 'flex-start'
|
||||||
}).component();
|
}).component();
|
||||||
const showMoreComponent = view.modelBuilder.hyperlink().withProperties({
|
const showMoreComponent = view.modelBuilder.hyperlink().withProperties({
|
||||||
@@ -253,29 +254,18 @@ export class DashboardWidget {
|
|||||||
CSSStyles: Object.assign({}, moreButtonStyle, {
|
CSSStyles: Object.assign({}, moreButtonStyle, {
|
||||||
'font-size': '12px',
|
'font-size': '12px',
|
||||||
'margin': '0px',
|
'margin': '0px',
|
||||||
'color': '#006ab1',
|
})
|
||||||
'padding-right': '5px'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
linkContainer.addItem(image, {
|
linkContainer.addItem(image, {
|
||||||
CSSStyles: {
|
CSSStyles: {
|
||||||
'padding': '0px',
|
'padding-left': '5px',
|
||||||
'padding-right': '5px',
|
'padding-top': '15px',
|
||||||
'padding-top': '5px',
|
|
||||||
'height': '10px',
|
|
||||||
'margin': '0px'
|
'margin': '0px'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
parentPanel.addItem(linkContainer, {
|
parentPanel.addItem(linkContainer, {
|
||||||
CSSStyles: {
|
CSSStyles: {}
|
||||||
'padding': '0px',
|
|
||||||
'padding-right': '5px',
|
|
||||||
'padding-top': '10px',
|
|
||||||
'height': '10px',
|
|
||||||
'margin': '0px'
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return showMoreComponent;
|
return showMoreComponent;
|
||||||
@@ -286,7 +276,6 @@ export class DashboardWidget {
|
|||||||
const videosContainer = view.modelBuilder.flexContainer().withLayout({
|
const videosContainer = view.modelBuilder.flexContainer().withLayout({
|
||||||
flexFlow: 'column',
|
flexFlow: 'column',
|
||||||
width: maxWidth,
|
width: maxWidth,
|
||||||
height: maxWidth,
|
|
||||||
justifyContent: 'flex-start'
|
justifyContent: 'flex-start'
|
||||||
}).component();
|
}).component();
|
||||||
const video1Container = view.modelBuilder.divContainer().withProperties({
|
const video1Container = view.modelBuilder.divContainer().withProperties({
|
||||||
@@ -299,7 +288,7 @@ export class DashboardWidget {
|
|||||||
width: maxWidth,
|
width: maxWidth,
|
||||||
height: '50px',
|
height: '50px',
|
||||||
CSSStyles: {
|
CSSStyles: {
|
||||||
'font-size': '12px',
|
'font-size': '13px',
|
||||||
'margin': '0px'
|
'margin': '0px'
|
||||||
}
|
}
|
||||||
}).component();
|
}).component();
|
||||||
@@ -310,11 +299,11 @@ export class DashboardWidget {
|
|||||||
});
|
});
|
||||||
videosContainer.addItem(video1Container, {
|
videosContainer.addItem(video1Container, {
|
||||||
CSSStyles: {
|
CSSStyles: {
|
||||||
'background-image': `url(${vscode.Uri.file(this.asAbsolutePath(<string>linkMetaData.iconPath?.light || ''))})`,
|
'background-image': `url(${vscode.Uri.file(this.asAbsolutePath(linkMetaData.iconPath?.light as string || ''))})`,
|
||||||
'background-repeat': 'no-repeat',
|
'background-repeat': 'no-repeat',
|
||||||
'background-position': 'top',
|
'background-position': 'top',
|
||||||
'width': `${maxWidth}px`,
|
'width': `${maxWidth}px`,
|
||||||
'height': '110px',
|
'height': '104px',
|
||||||
'background-size': `${maxWidth}px 120px`
|
'background-size': `${maxWidth}px 120px`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -384,20 +373,20 @@ export class DashboardWidget {
|
|||||||
linksContainer.addItems(links.map(l => this.createLink(view, l)), {
|
linksContainer.addItems(links.map(l => this.createLink(view, l)), {
|
||||||
CSSStyles: styles
|
CSSStyles: styles
|
||||||
});
|
});
|
||||||
moreLinksContainer.addItems(moreLinks.map(l => this.createLink(view, l)));
|
moreLinksContainer.addItems(moreLinks.map(l => this.createLink(view, l)), {
|
||||||
|
CSSStyles: styles
|
||||||
|
});
|
||||||
|
|
||||||
this.addShowMorePanel(view, linksContainer, moreLinksContainer, { 'padding-left': '10px' }, styles);
|
this.addShowMorePanel(view, linksContainer, moreLinksContainer, { 'padding-left': '10px', 'padding-top': '10px' }, {});
|
||||||
|
|
||||||
return linksContainer;
|
return linksContainer;
|
||||||
}
|
}
|
||||||
|
|
||||||
private createLink(view: azdata.ModelView, linkMetaData: IActionMetadata): azdata.Component {
|
private createLink(view: azdata.ModelView, linkMetaData: IActionMetadata): azdata.Component {
|
||||||
const maxHeight = 80;
|
|
||||||
const maxWidth = 400;
|
const maxWidth = 400;
|
||||||
const labelsContainer = view.modelBuilder.flexContainer().withLayout({
|
const labelsContainer = view.modelBuilder.flexContainer().withLayout({
|
||||||
flexFlow: 'column',
|
flexFlow: 'column',
|
||||||
width: maxWidth,
|
width: maxWidth,
|
||||||
height: maxHeight,
|
|
||||||
justifyContent: 'flex-start'
|
justifyContent: 'flex-start'
|
||||||
}).component();
|
}).component();
|
||||||
const descriptionComponent = view.modelBuilder.text().withProperties({
|
const descriptionComponent = view.modelBuilder.text().withProperties({
|
||||||
@@ -405,6 +394,7 @@ export class DashboardWidget {
|
|||||||
width: maxWidth,
|
width: maxWidth,
|
||||||
CSSStyles: {
|
CSSStyles: {
|
||||||
'font-size': '12px',
|
'font-size': '12px',
|
||||||
|
'line-height': '16px',
|
||||||
'margin': '0px'
|
'margin': '0px'
|
||||||
}
|
}
|
||||||
}).component();
|
}).component();
|
||||||
@@ -433,26 +423,20 @@ export class DashboardWidget {
|
|||||||
}).component();
|
}).component();
|
||||||
linkContainer.addItem(linkComponent, {
|
linkContainer.addItem(linkComponent, {
|
||||||
CSSStyles: {
|
CSSStyles: {
|
||||||
'padding': '0px',
|
'font-size': '14px',
|
||||||
'padding-right': '5px',
|
'line-height': '18px',
|
||||||
'margin': '0px',
|
'padding': '0 5px 0 0',
|
||||||
'color': '#006ab1'
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
linkContainer.addItem(image, {
|
linkContainer.addItem(image, {
|
||||||
CSSStyles: {
|
CSSStyles: {
|
||||||
'padding': '0px',
|
'padding': '5px 5px 0 0',
|
||||||
'padding-right': '5px',
|
|
||||||
'padding-top': '5px',
|
|
||||||
'height': '10px',
|
'height': '10px',
|
||||||
'margin': '0px'
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
labelsContainer.addItems([linkContainer, descriptionComponent], {
|
labelsContainer.addItems([linkContainer, descriptionComponent], {
|
||||||
CSSStyles: {
|
CSSStyles: {
|
||||||
'padding': '0px',
|
'padding': '5px 0 0 0',
|
||||||
'padding-top': '5px',
|
|
||||||
'margin': '0px'
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -466,8 +450,8 @@ export class DashboardWidget {
|
|||||||
private async createTasks(view: azdata.ModelView): Promise<azdata.Component> {
|
private async createTasks(view: azdata.ModelView): Promise<azdata.Component> {
|
||||||
const tasksContainer = view.modelBuilder.flexContainer().withLayout({
|
const tasksContainer = view.modelBuilder.flexContainer().withLayout({
|
||||||
flexFlow: 'row',
|
flexFlow: 'row',
|
||||||
|
height: '84px',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '50px',
|
|
||||||
}).component();
|
}).component();
|
||||||
const predictionMetadata: IActionMetadata = {
|
const predictionMetadata: IActionMetadata = {
|
||||||
title: constants.makePredictionTitle,
|
title: constants.makePredictionTitle,
|
||||||
@@ -502,11 +486,8 @@ export class DashboardWidget {
|
|||||||
command: constants.notebookCommandNew
|
command: constants.notebookCommandNew
|
||||||
};
|
};
|
||||||
const notebookModelsButton = this.createTaskButton(view, notebookMetadata);
|
const notebookModelsButton = this.createTaskButton(view, notebookMetadata);
|
||||||
tasksContainer.addItems([predictionButton, importModelsButton, notebookModelsButton], {
|
tasksContainer.addItems([predictionButton, importModelsButton, notebookModelsButton]);
|
||||||
CSSStyles: {
|
|
||||||
'padding': '10px'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!await this._predictService.serverSupportOnnxModel()) {
|
if (!await this._predictService.serverSupportOnnxModel()) {
|
||||||
console.log(constants.onnxNotSupportedError);
|
console.log(constants.onnxNotSupportedError);
|
||||||
}
|
}
|
||||||
@@ -515,87 +496,24 @@ export class DashboardWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private createTaskButton(view: azdata.ModelView, taskMetaData: IActionMetadata): azdata.Component {
|
private createTaskButton(view: azdata.ModelView, taskMetaData: IActionMetadata): azdata.Component {
|
||||||
const maxHeight = 116;
|
const maxHeight: number = 84;
|
||||||
const maxWidth = 250;
|
const maxWidth: number = 236;
|
||||||
const mainContainer = view.modelBuilder.divContainer().withLayout({
|
const buttonContainer = view.modelBuilder.button().withProperties<azdata.ButtonProperties>({
|
||||||
width: maxWidth,
|
buttonType: azdata.ButtonType.Informational,
|
||||||
height: maxHeight
|
description: taskMetaData.description,
|
||||||
}).withProperties({
|
height: maxHeight,
|
||||||
clickable: true,
|
iconHeight: 32,
|
||||||
ariaRole: taskMetaData.title
|
|
||||||
}).component();
|
|
||||||
const iconContainer = view.modelBuilder.flexContainer().withLayout({
|
|
||||||
flexFlow: 'row',
|
|
||||||
width: maxWidth,
|
|
||||||
height: maxHeight - 23,
|
|
||||||
alignItems: 'flex-start'
|
|
||||||
}).component();
|
|
||||||
const labelsContainer = view.modelBuilder.flexContainer().withLayout({
|
|
||||||
flexFlow: 'column',
|
|
||||||
width: maxWidth - 50,
|
|
||||||
height: maxHeight - 20,
|
|
||||||
justifyContent: 'space-between'
|
|
||||||
}).component();
|
|
||||||
const titleComponent = view.modelBuilder.text().withProperties({
|
|
||||||
value: taskMetaData.title,
|
|
||||||
CSSStyles: {
|
|
||||||
'font-size': '14px',
|
|
||||||
'font-weight': 'bold',
|
|
||||||
'margin': '0px'
|
|
||||||
}
|
|
||||||
}).component();
|
|
||||||
const descriptionComponent = view.modelBuilder.text().withProperties({
|
|
||||||
value: taskMetaData.description,
|
|
||||||
CSSStyles: {
|
|
||||||
'font-size': '13px',
|
|
||||||
'margin': '0px'
|
|
||||||
}
|
|
||||||
}).component();
|
|
||||||
const linkComponent = view.modelBuilder.hyperlink().withProperties({
|
|
||||||
label: constants.learnMoreTitle,
|
|
||||||
url: taskMetaData.link
|
|
||||||
}).component();
|
|
||||||
const image = view.modelBuilder.image().withProperties({
|
|
||||||
width: '20px',
|
|
||||||
height: '20px',
|
|
||||||
iconPath: taskMetaData.iconPath,
|
iconPath: taskMetaData.iconPath,
|
||||||
iconHeight: '20px',
|
iconWidth: 32,
|
||||||
iconWidth: '20px'
|
label: taskMetaData.title,
|
||||||
|
title: taskMetaData.title,
|
||||||
|
width: maxWidth,
|
||||||
}).component();
|
}).component();
|
||||||
labelsContainer.addItems([titleComponent, descriptionComponent, linkComponent], {
|
buttonContainer.onDidClick(async () => {
|
||||||
CSSStyles: {
|
if (buttonContainer.enabled && taskMetaData.command) {
|
||||||
'padding': '0px',
|
|
||||||
'padding-bottom': '5px',
|
|
||||||
'width': '200px',
|
|
||||||
'margin': '0px',
|
|
||||||
'color': '#006ab1'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
iconContainer.addItem(image, {
|
|
||||||
CSSStyles: {
|
|
||||||
'padding-top': '10px',
|
|
||||||
'padding-right': '10px'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
iconContainer.addItem(labelsContainer, {
|
|
||||||
CSSStyles: {
|
|
||||||
'padding-top': '5px',
|
|
||||||
'padding-right': '10px'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
mainContainer.addItems([iconContainer], {
|
|
||||||
CSSStyles: {
|
|
||||||
'padding': '10px',
|
|
||||||
'border-radius': '5px',
|
|
||||||
'border-color': '#f2f2f2',
|
|
||||||
'border': '1px solid'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
mainContainer.onDidClick(async () => {
|
|
||||||
if (mainContainer.enabled && taskMetaData.command) {
|
|
||||||
await this._apiWrapper.executeCommand(taskMetaData.command);
|
await this._apiWrapper.executeCommand(taskMetaData.command);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return mainContainer;
|
return view.modelBuilder.divContainer().withItems([buttonContainer]).component();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}",
|
"downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}",
|
||||||
"version": "3.0.0-release.29",
|
"version": "3.0.0-release.38",
|
||||||
"downloadFileNames": {
|
"downloadFileNames": {
|
||||||
"Windows_86": "win-x86-netcoreapp3.1.zip",
|
"Windows_86": "win-x86-netcoreapp3.1.zip",
|
||||||
"Windows_64": "win-x64-netcoreapp3.1.zip",
|
"Windows_64": "win-x64-netcoreapp3.1.zip",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import * as azdata from 'azdata';
|
import * as azdata from 'azdata';
|
||||||
|
import * as bdc from 'bdc';
|
||||||
import * as nls from 'vscode-nls';
|
import * as nls from 'vscode-nls';
|
||||||
const localize = nls.loadMessageBundle();
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
@@ -23,15 +24,15 @@ const hyperlinkedEndpoints = [grafanaEndpointName, logsuiEndpointName, sparkHist
|
|||||||
|
|
||||||
export function registerServiceEndpoints(context: vscode.ExtensionContext): void {
|
export function registerServiceEndpoints(context: vscode.ExtensionContext): void {
|
||||||
azdata.ui.registerModelViewProvider('bdc-endpoints', async (view) => {
|
azdata.ui.registerModelViewProvider('bdc-endpoints', async (view) => {
|
||||||
let endpointsArray: Array<utils.IEndpoint> = Object.assign([], utils.getClusterEndpoints(view.serverInfo));
|
let endpointsArray: Array<bdc.IEndpointModel> = Object.assign([], utils.getClusterEndpoints(view.serverInfo));
|
||||||
|
|
||||||
if (endpointsArray.length > 0) {
|
if (endpointsArray.length > 0) {
|
||||||
const grafanaEp = endpointsArray.find(e => e.serviceName === grafanaEndpointName);
|
const grafanaEp = endpointsArray.find(e => e.name === grafanaEndpointName);
|
||||||
if (grafanaEp && grafanaEp.endpoint && grafanaEp.endpoint.indexOf('/d/wZx3OUdmz') === -1) {
|
if (grafanaEp && grafanaEp.endpoint && grafanaEp.endpoint.indexOf('/d/wZx3OUdmz') === -1) {
|
||||||
// Update to have correct URL
|
// Update to have correct URL
|
||||||
grafanaEp.endpoint += '/d/wZx3OUdmz';
|
grafanaEp.endpoint += '/d/wZx3OUdmz';
|
||||||
}
|
}
|
||||||
const kibanaEp = endpointsArray.find(e => e.serviceName === logsuiEndpointName);
|
const kibanaEp = endpointsArray.find(e => e.name === logsuiEndpointName);
|
||||||
if (kibanaEp && kibanaEp.endpoint && kibanaEp.endpoint.indexOf('/app/kibana#/discover') === -1) {
|
if (kibanaEp && kibanaEp.endpoint && kibanaEp.endpoint.indexOf('/app/kibana#/discover') === -1) {
|
||||||
// Update to have correct URL
|
// Update to have correct URL
|
||||||
kibanaEp.endpoint += '/app/kibana#/discover';
|
kibanaEp.endpoint += '/app/kibana#/discover';
|
||||||
@@ -40,13 +41,13 @@ export function registerServiceEndpoints(context: vscode.ExtensionContext): void
|
|||||||
if (!grafanaEp) {
|
if (!grafanaEp) {
|
||||||
// We are on older CTP, need to manually add some endpoints.
|
// We are on older CTP, need to manually add some endpoints.
|
||||||
// TODO remove once CTP support goes away
|
// TODO remove once CTP support goes away
|
||||||
const managementProxyEp = endpointsArray.find(e => e.serviceName === mgmtProxyName);
|
const managementProxyEp = endpointsArray.find(e => e.name === mgmtProxyName);
|
||||||
if (managementProxyEp) {
|
if (managementProxyEp) {
|
||||||
endpointsArray.push(getCustomEndpoint(managementProxyEp, grafanaEndpointName, grafanaDescription, '/grafana/d/wZx3OUdmz'));
|
endpointsArray.push(getCustomEndpoint(managementProxyEp, grafanaEndpointName, grafanaDescription, '/grafana/d/wZx3OUdmz'));
|
||||||
endpointsArray.push(getCustomEndpoint(managementProxyEp, logsuiEndpointName, logsuiDescription, '/kibana/app/kibana#/discover'));
|
endpointsArray.push(getCustomEndpoint(managementProxyEp, logsuiEndpointName, logsuiDescription, '/kibana/app/kibana#/discover'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const gatewayEp = endpointsArray.find(e => e.serviceName === 'gateway');
|
const gatewayEp = endpointsArray.find(e => e.name === 'gateway');
|
||||||
if (gatewayEp) {
|
if (gatewayEp) {
|
||||||
endpointsArray.push(getCustomEndpoint(gatewayEp, sparkHistoryEndpointName, sparkHistoryDescription, '/gateway/default/sparkhistory'));
|
endpointsArray.push(getCustomEndpoint(gatewayEp, sparkHistoryEndpointName, sparkHistoryDescription, '/gateway/default/sparkhistory'));
|
||||||
endpointsArray.push(getCustomEndpoint(gatewayEp, yarnUiEndpointName, yarnHistoryDescription, '/gateway/default/yarn'));
|
endpointsArray.push(getCustomEndpoint(gatewayEp, yarnUiEndpointName, yarnHistoryDescription, '/gateway/default/yarn'));
|
||||||
@@ -54,14 +55,14 @@ export function registerServiceEndpoints(context: vscode.ExtensionContext): void
|
|||||||
}
|
}
|
||||||
|
|
||||||
endpointsArray = endpointsArray.map(e => {
|
endpointsArray = endpointsArray.map(e => {
|
||||||
e.description = getEndpointDisplayText(e.serviceName, e.description);
|
e.description = getEndpointDisplayText(e.name, e.description);
|
||||||
return e;
|
return e;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort the endpoints. The sort method is that SQL Server Master is first - followed by all
|
// Sort the endpoints. The sort method is that SQL Server Master is first - followed by all
|
||||||
// others in alphabetical order by endpoint
|
// others in alphabetical order by endpoint
|
||||||
const sqlServerMasterEndpoints = endpointsArray.filter(e => e.serviceName === Endpoint.sqlServerMaster);
|
const sqlServerMasterEndpoints = endpointsArray.filter(e => e.name === Endpoint.sqlServerMaster);
|
||||||
endpointsArray = endpointsArray.filter(e => e.serviceName !== Endpoint.sqlServerMaster)
|
endpointsArray = endpointsArray.filter(e => e.name !== Endpoint.sqlServerMaster)
|
||||||
.sort((e1, e2) => e1.endpoint.localeCompare(e2.endpoint));
|
.sort((e1, e2) => e1.endpoint.localeCompare(e2.endpoint));
|
||||||
endpointsArray.unshift(...sqlServerMasterEndpoints);
|
endpointsArray.unshift(...sqlServerMasterEndpoints);
|
||||||
|
|
||||||
@@ -70,7 +71,7 @@ export function registerServiceEndpoints(context: vscode.ExtensionContext): void
|
|||||||
const endPointRow = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component();
|
const endPointRow = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component();
|
||||||
const nameCell = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: endpointInfo.description }).component();
|
const nameCell = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: endpointInfo.description }).component();
|
||||||
endPointRow.addItem(nameCell, { CSSStyles: { 'width': '35%', 'font-weight': '600', 'user-select': 'text' } });
|
endPointRow.addItem(nameCell, { CSSStyles: { 'width': '35%', 'font-weight': '600', 'user-select': 'text' } });
|
||||||
if (hyperlinkedEndpoints.findIndex(e => e === endpointInfo.serviceName) >= 0) {
|
if (hyperlinkedEndpoints.findIndex(e => e === endpointInfo.name) >= 0) {
|
||||||
const linkCell = view.modelBuilder.hyperlink()
|
const linkCell = view.modelBuilder.hyperlink()
|
||||||
.withProperties<azdata.HyperlinkComponentProperties>({
|
.withProperties<azdata.HyperlinkComponentProperties>({
|
||||||
label: endpointInfo.endpoint,
|
label: endpointInfo.endpoint,
|
||||||
@@ -111,10 +112,10 @@ export function registerServiceEndpoints(context: vscode.ExtensionContext): void
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCustomEndpoint(parentEndpoint: utils.IEndpoint, serviceName: string, description: string, serviceUrl?: string): utils.IEndpoint {
|
function getCustomEndpoint(parentEndpoint: bdc.IEndpointModel, serviceName: string, description: string, serviceUrl?: string): bdc.IEndpointModel {
|
||||||
if (parentEndpoint) {
|
if (parentEndpoint) {
|
||||||
let endpoint: utils.IEndpoint = {
|
let endpoint: bdc.IEndpointModel = {
|
||||||
serviceName: serviceName,
|
name: serviceName,
|
||||||
description: description,
|
description: description,
|
||||||
endpoint: parentEndpoint.endpoint + serviceUrl,
|
endpoint: parentEndpoint.endpoint + serviceUrl,
|
||||||
protocol: 'https'
|
protocol: 'https'
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ async function handleOpenNotebookTask(profile: azdata.IConnectionProfile): Promi
|
|||||||
|
|
||||||
async function handleOpenClusterDashboardTask(profile: azdata.IConnectionProfile, appContext: AppContext): Promise<void> {
|
async function handleOpenClusterDashboardTask(profile: azdata.IConnectionProfile, appContext: AppContext): Promise<void> {
|
||||||
const serverInfo = await azdata.connection.getServerInfo(profile.id);
|
const serverInfo = await azdata.connection.getServerInfo(profile.id);
|
||||||
const controller = Utils.getClusterEndpoints(serverInfo).find(e => e.serviceName === Endpoint.controller);
|
const controller = Utils.getClusterEndpoints(serverInfo).find(e => e.name === Endpoint.controller);
|
||||||
if (!controller) {
|
if (!controller) {
|
||||||
vscode.window.showErrorMessage(localize('noController', "Could not find the controller endpoint for this instance"));
|
vscode.window.showErrorMessage(localize('noController', "Could not find the controller endpoint for this instance"));
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -406,7 +406,7 @@ export class ManageAccessCommand extends Command {
|
|||||||
try {
|
try {
|
||||||
let node = await getNode<HdfsFileSourceNode>(context, this.appContext);
|
let node = await getNode<HdfsFileSourceNode>(context, this.appContext);
|
||||||
if (node) {
|
if (node) {
|
||||||
new ManageAccessDialog(node.hdfsPath, node.fileSource).openDialog();
|
new ManageAccessDialog(node.hdfsPath, await node.getFileSource()).openDialog();
|
||||||
} else {
|
} else {
|
||||||
vscode.window.showErrorMessage(LocalizedConstants.msgMissingNodeContext);
|
vscode.window.showErrorMessage(LocalizedConstants.msgMissingNodeContext);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { TreeNode } from './treeNodes';
|
|||||||
import * as utils from '../utils';
|
import * as utils from '../utils';
|
||||||
import { IFileNode } from './types';
|
import { IFileNode } from './types';
|
||||||
import { MountStatus } from '../hdfs/mount';
|
import { MountStatus } from '../hdfs/mount';
|
||||||
|
import { SqlClusterSession } from './objectExplorerNodeProvider';
|
||||||
|
|
||||||
export interface ITreeChangeHandler {
|
export interface ITreeChangeHandler {
|
||||||
notifyNodeChanged(node: TreeNode): void;
|
notifyNodeChanged(node: TreeNode): void;
|
||||||
@@ -29,8 +30,8 @@ export class TreeDataContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export abstract class HdfsFileSourceNode extends TreeNode {
|
export abstract class HdfsFileSourceNode extends TreeNode {
|
||||||
constructor(protected context: TreeDataContext, protected _path: string, public readonly fileSource: IFileSource, protected mountStatus?: MountStatus) {
|
constructor(protected context: TreeDataContext, protected _path: string, fileSource: IFileSource | undefined, protected mountStatus?: MountStatus) {
|
||||||
super();
|
super(fileSource);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get hdfsPath(): string {
|
public get hdfsPath(): string {
|
||||||
@@ -51,7 +52,8 @@ export abstract class HdfsFileSourceNode extends TreeNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async delete(recursive: boolean = false): Promise<void> {
|
public async delete(recursive: boolean = false): Promise<void> {
|
||||||
await this.fileSource.delete(this.hdfsPath, recursive);
|
const fileSource = await this.getFileSource();
|
||||||
|
await fileSource.delete(this.hdfsPath, recursive);
|
||||||
// Notify parent should be updated. If at top, will return undefined which will refresh whole tree
|
// Notify parent should be updated. If at top, will return undefined which will refresh whole tree
|
||||||
(<HdfsFileSourceNode>this.parent).onChildRemoved();
|
(<HdfsFileSourceNode>this.parent).onChildRemoved();
|
||||||
this.context.changeHandler.notifyNodeChanged(this.parent);
|
this.context.changeHandler.notifyNodeChanged(this.parent);
|
||||||
@@ -60,34 +62,28 @@ export abstract class HdfsFileSourceNode extends TreeNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class FolderNode extends HdfsFileSourceNode {
|
export class FolderNode extends HdfsFileSourceNode {
|
||||||
private children: TreeNode[];
|
private children: TreeNode[] = [];
|
||||||
protected _nodeType: string;
|
protected _nodeType: string;
|
||||||
constructor(context: TreeDataContext, path: string, fileSource: IFileSource, nodeType?: string, mountStatus?: MountStatus) {
|
constructor(context: TreeDataContext, path: string, fileSource: IFileSource | undefined, nodeType?: string, mountStatus?: MountStatus) {
|
||||||
super(context, path, fileSource, mountStatus);
|
super(context, path, fileSource, mountStatus);
|
||||||
this._nodeType = nodeType ? nodeType : Constants.MssqlClusterItems.Folder;
|
this._nodeType = nodeType ? nodeType : Constants.MssqlClusterItems.Folder;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ensureChildrenExist(): void {
|
|
||||||
if (!this.children) {
|
|
||||||
this.children = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public onChildRemoved(): void {
|
public onChildRemoved(): void {
|
||||||
this.children = undefined;
|
this.children = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getChildren(refreshChildren: boolean): Promise<TreeNode[]> {
|
async getChildren(refreshChildren: boolean): Promise<TreeNode[]> {
|
||||||
if (refreshChildren || !this.children) {
|
if (refreshChildren || !this.children) {
|
||||||
this.ensureChildrenExist();
|
|
||||||
try {
|
try {
|
||||||
let files: IFile[] = await this.fileSource.enumerateFiles(this._path);
|
const fileSource = await this.getFileSource();
|
||||||
|
let files: IFile[] = await fileSource.enumerateFiles(this._path);
|
||||||
if (files) {
|
if (files) {
|
||||||
// Note: for now, assuming HDFS-provided sorting is sufficient
|
// Note: for now, assuming HDFS-provided sorting is sufficient
|
||||||
this.children = files.map((file) => {
|
this.children = files.map((file) => {
|
||||||
let node: TreeNode = file.fileType === FileType.File ?
|
let node: TreeNode = file.fileType === FileType.File ?
|
||||||
new FileNode(this.context, file.path, this.fileSource, this.getChildMountStatus(file)) :
|
new FileNode(this.context, file.path, fileSource, this.getChildMountStatus(file)) :
|
||||||
new FolderNode(this.context, file.path, this.fileSource, Constants.MssqlClusterItems.Folder, this.getChildMountStatus(file));
|
new FolderNode(this.context, file.path, fileSource, Constants.MssqlClusterItems.Folder, this.getChildMountStatus(file));
|
||||||
node.parent = this;
|
node.parent = this;
|
||||||
return node;
|
return node;
|
||||||
});
|
});
|
||||||
@@ -153,8 +149,9 @@ export class FolderNode extends HdfsFileSourceNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async writeFileAsync(localFile: IFile): Promise<FileNode> {
|
private async writeFileAsync(localFile: IFile): Promise<FileNode> {
|
||||||
await this.fileSource.writeFile(localFile, this._path);
|
const fileSource = await this.getFileSource();
|
||||||
let fileNode = new FileNode(this.context, File.createPath(this._path, File.getBasename(localFile)), this.fileSource);
|
await fileSource.writeFile(localFile, this._path);
|
||||||
|
let fileNode = new FileNode(this.context, File.createPath(this._path, File.getBasename(localFile)), fileSource);
|
||||||
return fileNode;
|
return fileNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,8 +160,9 @@ export class FolderNode extends HdfsFileSourceNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async mkdirAsync(name: string): Promise<FolderNode> {
|
private async mkdirAsync(name: string): Promise<FolderNode> {
|
||||||
await this.fileSource.mkdir(name, this._path);
|
const fileSource = await this.getFileSource();
|
||||||
let subDir = new FolderNode(this.context, File.createPath(this._path, name), this.fileSource);
|
await fileSource.mkdir(name, this._path);
|
||||||
|
let subDir = new FolderNode(this.context, File.createPath(this._path, name), fileSource);
|
||||||
return subDir;
|
return subDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,8 +184,8 @@ export class FolderNode extends HdfsFileSourceNode {
|
|||||||
|
|
||||||
export class ConnectionNode extends FolderNode {
|
export class ConnectionNode extends FolderNode {
|
||||||
|
|
||||||
constructor(context: TreeDataContext, private displayName: string, fileSource: IFileSource) {
|
constructor(context: TreeDataContext, private displayName: string, private clusterSession: SqlClusterSession) {
|
||||||
super(context, '/', fileSource, Constants.MssqlClusterItems.Connection);
|
super(context, '/', undefined, Constants.MssqlClusterItems.Connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
getDisplayName(): string {
|
getDisplayName(): string {
|
||||||
@@ -204,6 +202,16 @@ export class ConnectionNode extends FolderNode {
|
|||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getFileSource(): Promise<IFileSource | undefined> {
|
||||||
|
// The node is initially created without a filesource and then one is created only once an action is
|
||||||
|
// taken that requires a connection
|
||||||
|
const fileSource = await super.getFileSource();
|
||||||
|
if (!fileSource) {
|
||||||
|
await this.updateFileSource(await this.clusterSession.getSqlClusterConnection());
|
||||||
|
}
|
||||||
|
return super.getFileSource();
|
||||||
|
}
|
||||||
|
|
||||||
getNodeInfo(): azdata.NodeInfo {
|
getNodeInfo(): azdata.NodeInfo {
|
||||||
// TODO handle error message case by returning it in the OE API
|
// TODO handle error message case by returning it in the OE API
|
||||||
// TODO support better mapping of node type
|
// TODO support better mapping of node type
|
||||||
@@ -264,18 +272,21 @@ export class FileNode extends HdfsFileSourceNode implements IFileNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getFileContentsAsString(maxBytes?: number): Promise<string> {
|
public async getFileContentsAsString(maxBytes?: number): Promise<string> {
|
||||||
let contents: Buffer = await this.fileSource.readFile(this.hdfsPath, maxBytes);
|
const fileSource = await this.getFileSource();
|
||||||
|
let contents: Buffer = await fileSource.readFile(this.hdfsPath, maxBytes);
|
||||||
return contents ? contents.toString('utf8') : '';
|
return contents ? contents.toString('utf8') : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getFileLinesAsString(maxLines: number): Promise<string> {
|
public async getFileLinesAsString(maxLines: number): Promise<string> {
|
||||||
let contents: Buffer = await this.fileSource.readFileLines(this.hdfsPath, maxLines);
|
const fileSource = await this.getFileSource();
|
||||||
|
let contents: Buffer = await fileSource.readFileLines(this.hdfsPath, maxLines);
|
||||||
return contents ? contents.toString('utf8') : '';
|
return contents ? contents.toString('utf8') : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
public writeFileContentsToDisk(localPath: string, cancelToken?: vscode.CancellationTokenSource): Promise<vscode.Uri> {
|
public async writeFileContentsToDisk(localPath: string, cancelToken?: vscode.CancellationTokenSource): Promise<vscode.Uri> {
|
||||||
|
const fileSource = await this.getFileSource();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let readStream: fs.ReadStream = this.fileSource.createReadStream(this.hdfsPath);
|
let readStream: fs.ReadStream = fileSource.createReadStream(this.hdfsPath);
|
||||||
readStream.on('error', (err) => {
|
readStream.on('error', (err) => {
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
@@ -320,7 +331,7 @@ class ErrorNode extends TreeNode {
|
|||||||
|
|
||||||
private _nodePathValue: string;
|
private _nodePathValue: string;
|
||||||
constructor(private message: string) {
|
constructor(private message: string) {
|
||||||
super();
|
super(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static create(message: string, parent: TreeNode, errorCode?: number): ErrorNode {
|
public static create(message: string, parent: TreeNode, errorCode?: number): ErrorNode {
|
||||||
|
|||||||
@@ -13,23 +13,22 @@ import { SqlClusterConnection } from './connection';
|
|||||||
import * as utils from '../utils';
|
import * as utils from '../utils';
|
||||||
import { TreeNode } from './treeNodes';
|
import { TreeNode } from './treeNodes';
|
||||||
import { ConnectionNode, TreeDataContext, ITreeChangeHandler } from './hdfsProvider';
|
import { ConnectionNode, TreeDataContext, ITreeChangeHandler } from './hdfsProvider';
|
||||||
import { IFileSource } from './fileSources';
|
|
||||||
import { AppContext } from '../appContext';
|
import { AppContext } from '../appContext';
|
||||||
import * as constants from '../constants';
|
import * as constants from '../constants';
|
||||||
import * as SqlClusterLookUp from '../sqlClusterLookUp';
|
|
||||||
import { ICommandObjectExplorerContext } from './command';
|
import { ICommandObjectExplorerContext } from './command';
|
||||||
import { IPrompter, IQuestion, QuestionTypes } from '../prompts/question';
|
import { IPrompter, IQuestion, QuestionTypes } from '../prompts/question';
|
||||||
|
import { getSqlClusterConnectionParams } from '../sqlClusterLookUp';
|
||||||
|
|
||||||
export const mssqlOutputChannel = vscode.window.createOutputChannel(constants.providerId);
|
export const mssqlOutputChannel = vscode.window.createOutputChannel(constants.providerId);
|
||||||
|
|
||||||
export class MssqlObjectExplorerNodeProvider extends ProviderBase implements azdata.ObjectExplorerNodeProvider, ITreeChangeHandler {
|
export class MssqlObjectExplorerNodeProvider extends ProviderBase implements azdata.ObjectExplorerNodeProvider, ITreeChangeHandler {
|
||||||
public readonly supportedProviderId: string = constants.providerId;
|
public readonly supportedProviderId: string = constants.providerId;
|
||||||
private sessionMap: Map<string, SqlClusterSession>;
|
private clusterSessionMap: Map<string, SqlClusterSession>;
|
||||||
private expandCompleteEmitter = new vscode.EventEmitter<azdata.ObjectExplorerExpandInfo>();
|
private expandCompleteEmitter = new vscode.EventEmitter<azdata.ObjectExplorerExpandInfo>();
|
||||||
|
|
||||||
constructor(private prompter: IPrompter, private appContext: AppContext) {
|
constructor(private prompter: IPrompter, private appContext: AppContext) {
|
||||||
super();
|
super();
|
||||||
this.sessionMap = new Map<string, SqlClusterSession>();
|
this.clusterSessionMap = new Map<string, SqlClusterSession>();
|
||||||
this.appContext.registerService<MssqlObjectExplorerNodeProvider>(constants.ObjectExplorerService, this);
|
this.appContext.registerService<MssqlObjectExplorerNodeProvider>(constants.ObjectExplorerService, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,12 +48,11 @@ export class MssqlObjectExplorerNodeProvider extends ProviderBase implements azd
|
|||||||
let sqlConnProfile = await azdata.objectexplorer.getSessionConnectionProfile(session.sessionId);
|
let sqlConnProfile = await azdata.objectexplorer.getSessionConnectionProfile(session.sessionId);
|
||||||
if (!sqlConnProfile) { return false; }
|
if (!sqlConnProfile) { return false; }
|
||||||
|
|
||||||
let clusterConnInfo = await SqlClusterLookUp.getSqlClusterConnection(sqlConnProfile);
|
const isBigDataCluster = await utils.isBigDataCluster(sqlConnProfile.id);
|
||||||
if (!clusterConnInfo) { return false; }
|
if (!isBigDataCluster) { return false; }
|
||||||
|
|
||||||
let clusterConnection = new SqlClusterConnection(clusterConnInfo);
|
let clusterSession = new SqlClusterSession(session, sqlConnProfile, this.appContext, this);
|
||||||
let clusterSession = new SqlClusterSession(clusterConnection, session, sqlConnProfile, this.appContext, this);
|
this.clusterSessionMap.set(session.sessionId, clusterSession);
|
||||||
this.sessionMap.set(session.sessionId, clusterSession);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +67,7 @@ export class MssqlObjectExplorerNodeProvider extends ProviderBase implements azd
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async doExpandNode(nodeInfo: azdata.ExpandNodeInfo, isRefresh: boolean = false): Promise<boolean> {
|
private async doExpandNode(nodeInfo: azdata.ExpandNodeInfo, isRefresh: boolean = false): Promise<boolean> {
|
||||||
let session = this.sessionMap.get(nodeInfo.sessionId);
|
let session = this.clusterSessionMap.get(nodeInfo.sessionId);
|
||||||
let response: azdata.ObjectExplorerExpandInfo = {
|
let response: azdata.ObjectExplorerExpandInfo = {
|
||||||
sessionId: nodeInfo.sessionId,
|
sessionId: nodeInfo.sessionId,
|
||||||
nodePath: nodeInfo.nodePath,
|
nodePath: nodeInfo.nodePath,
|
||||||
@@ -117,20 +115,31 @@ export class MssqlObjectExplorerNodeProvider extends ProviderBase implements azd
|
|||||||
// Only child returned when failure happens : When failed with 'Unauthorized' error, prompt for password.
|
// Only child returned when failure happens : When failed with 'Unauthorized' error, prompt for password.
|
||||||
if (children.length === 1 && this.hasExpansionError(children)) {
|
if (children.length === 1 && this.hasExpansionError(children)) {
|
||||||
if (children[0].errorStatusCode === 401) {
|
if (children[0].errorStatusCode === 401) {
|
||||||
|
const sqlClusterConnection = await session.getSqlClusterConnection();
|
||||||
// First prompt for username (defaulting to existing username)
|
// First prompt for username (defaulting to existing username)
|
||||||
let username: string = await this.promptInput(localize('promptUsername', "Please provide the username to connect to HDFS:"), session.sqlClusterConnection.user);
|
let username = await this.prompter.promptSingle<string>(<IQuestion>{
|
||||||
|
type: QuestionTypes.input,
|
||||||
|
name: 'inputPrompt',
|
||||||
|
message: localize('promptUsername', "Please provide the username to connect to HDFS:"),
|
||||||
|
default: sqlClusterConnection.user
|
||||||
|
});
|
||||||
// Only update the username if it's different than the original (the update functions ignore falsy values)
|
// Only update the username if it's different than the original (the update functions ignore falsy values)
|
||||||
if (username === session.sqlClusterConnection.user) {
|
if (username === sqlClusterConnection.user) {
|
||||||
username = '';
|
username = '';
|
||||||
}
|
}
|
||||||
session.sqlClusterConnection.updateUsername(username);
|
sqlClusterConnection.updateUsername(username);
|
||||||
|
|
||||||
// And then prompt for password
|
// And then prompt for password
|
||||||
const password: string = await this.promptPassword(localize('prmptPwd', "Please provide the password to connect to HDFS:"));
|
const password = await this.prompter.promptSingle<string>(<IQuestion>{
|
||||||
session.sqlClusterConnection.updatePassword(password);
|
type: QuestionTypes.password,
|
||||||
|
name: 'passwordPrompt',
|
||||||
|
message: localize('prmptPwd', "Please provide the password to connect to HDFS:"),
|
||||||
|
default: ''
|
||||||
|
});
|
||||||
|
sqlClusterConnection.updatePassword(password);
|
||||||
|
|
||||||
if (username || password) {
|
if (username || password) {
|
||||||
await node.updateFileSource(session.sqlClusterConnection);
|
await node.updateFileSource(sqlClusterConnection);
|
||||||
children = await node.getChildren(true);
|
children = await node.getChildren(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,31 +159,13 @@ export class MssqlObjectExplorerNodeProvider extends ProviderBase implements azd
|
|||||||
this.expandCompleteEmitter.fire(expandResult);
|
this.expandCompleteEmitter.fire(expandResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async promptInput(promptMsg: string, defaultValue: string): Promise<string> {
|
|
||||||
return await this.prompter.promptSingle(<IQuestion>{
|
|
||||||
type: QuestionTypes.input,
|
|
||||||
name: 'inputPrompt',
|
|
||||||
message: promptMsg,
|
|
||||||
default: defaultValue
|
|
||||||
}).then(confirmed => <string>confirmed);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async promptPassword(promptMsg: string): Promise<string> {
|
|
||||||
return await this.prompter.promptSingle(<IQuestion>{
|
|
||||||
type: QuestionTypes.password,
|
|
||||||
name: 'passwordPrompt',
|
|
||||||
message: promptMsg,
|
|
||||||
default: ''
|
|
||||||
}).then(confirmed => <string>confirmed);
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshNode(nodeInfo: azdata.ExpandNodeInfo): Thenable<boolean> {
|
refreshNode(nodeInfo: azdata.ExpandNodeInfo): Thenable<boolean> {
|
||||||
// TODO #3815 implement properly
|
// TODO #3815 implement properly
|
||||||
return this.expandNode(nodeInfo, true);
|
return this.expandNode(nodeInfo, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSessionClose(closeSessionInfo: azdata.ObjectExplorerCloseSessionInfo): void {
|
handleSessionClose(closeSessionInfo: azdata.ObjectExplorerCloseSessionInfo): void {
|
||||||
this.sessionMap.delete(closeSessionInfo.sessionId);
|
this.clusterSessionMap.delete(closeSessionInfo.sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
findNodes(findNodesInfo: azdata.FindNodesInfo): Thenable<azdata.ObjectExplorerFindNodesResponse> {
|
findNodes(findNodesInfo: azdata.FindNodesInfo): Thenable<azdata.ObjectExplorerFindNodesResponse> {
|
||||||
@@ -241,8 +232,8 @@ export class MssqlObjectExplorerNodeProvider extends ProviderBase implements azd
|
|||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
public findSqlClusterSessionBySqlConnProfile(connectionProfile: azdata.IConnectionProfile): SqlClusterSession {
|
public findSqlClusterSessionBySqlConnProfile(connectionProfile: azdata.IConnectionProfile): SqlClusterSession | undefined {
|
||||||
for (let session of this.sessionMap.values()) {
|
for (let session of this.clusterSessionMap.values()) {
|
||||||
if (session.isMatchedSqlConnection(connectionProfile)) {
|
if (session.isMatchedSqlConnection(connectionProfile)) {
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
@@ -251,11 +242,10 @@ export class MssqlObjectExplorerNodeProvider extends ProviderBase implements azd
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SqlClusterSession {
|
export class SqlClusterSession {
|
||||||
private _rootNode: SqlClusterRootNode;
|
private _rootNode: SqlClusterRootNode;
|
||||||
|
private _sqlClusterConnection: SqlClusterConnection | undefined = undefined;
|
||||||
constructor(
|
constructor(
|
||||||
private _sqlClusterConnection: SqlClusterConnection,
|
|
||||||
private _sqlSession: azdata.ObjectExplorerSession,
|
private _sqlSession: azdata.ObjectExplorerSession,
|
||||||
private _sqlConnectionProfile: azdata.IConnectionProfile,
|
private _sqlConnectionProfile: azdata.IConnectionProfile,
|
||||||
private _appContext: AppContext,
|
private _appContext: AppContext,
|
||||||
@@ -266,7 +256,13 @@ class SqlClusterSession {
|
|||||||
this._sqlSession.rootNode.nodePath);
|
this._sqlSession.rootNode.nodePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get sqlClusterConnection(): SqlClusterConnection { return this._sqlClusterConnection; }
|
public async getSqlClusterConnection(): Promise<SqlClusterConnection> {
|
||||||
|
if (!this._sqlClusterConnection) {
|
||||||
|
const sqlClusterConnectionParams = await getSqlClusterConnectionParams(this._sqlConnectionProfile, this._appContext);
|
||||||
|
this._sqlClusterConnection = new SqlClusterConnection(sqlClusterConnectionParams);
|
||||||
|
}
|
||||||
|
return this._sqlClusterConnection;
|
||||||
|
}
|
||||||
public get sqlSession(): azdata.ObjectExplorerSession { return this._sqlSession; }
|
public get sqlSession(): azdata.ObjectExplorerSession { return this._sqlSession; }
|
||||||
public get sqlConnectionProfile(): azdata.IConnectionProfile { return this._sqlConnectionProfile; }
|
public get sqlConnectionProfile(): azdata.IConnectionProfile { return this._sqlConnectionProfile; }
|
||||||
public get sessionId(): string { return this._sqlSession.sessionId; }
|
public get sessionId(): string { return this._sqlSession.sessionId; }
|
||||||
@@ -284,7 +280,7 @@ class SqlClusterRootNode extends TreeNode {
|
|||||||
private _treeDataContext: TreeDataContext,
|
private _treeDataContext: TreeDataContext,
|
||||||
private _nodePathValue: string
|
private _nodePathValue: string
|
||||||
) {
|
) {
|
||||||
super();
|
super(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get session(): SqlClusterSession {
|
public get session(): SqlClusterSession {
|
||||||
@@ -304,8 +300,8 @@ class SqlClusterRootNode extends TreeNode {
|
|||||||
|
|
||||||
private async refreshChildren(): Promise<TreeNode[]> {
|
private async refreshChildren(): Promise<TreeNode[]> {
|
||||||
this._children = [];
|
this._children = [];
|
||||||
let fileSource: IFileSource = await this.session.sqlClusterConnection.createHdfsFileSource();
|
|
||||||
let hdfsNode = new ConnectionNode(this._treeDataContext, localize('hdfsFolder', "HDFS"), fileSource);
|
let hdfsNode = new ConnectionNode(this._treeDataContext, localize('hdfsFolder', "HDFS"), this.session);
|
||||||
hdfsNode.parent = this;
|
hdfsNode.parent = this;
|
||||||
this._children.push(hdfsNode);
|
this._children.push(hdfsNode);
|
||||||
return this._children;
|
return this._children;
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ type TreeNodePredicate = (node: TreeNode) => boolean;
|
|||||||
|
|
||||||
export abstract class TreeNode implements ITreeNode {
|
export abstract class TreeNode implements ITreeNode {
|
||||||
private _parent: TreeNode = undefined;
|
private _parent: TreeNode = undefined;
|
||||||
protected fileSource: IFileSource;
|
|
||||||
private _errorStatusCode: number;
|
private _errorStatusCode: number;
|
||||||
|
|
||||||
|
constructor(private _fileSource: IFileSource | undefined) { }
|
||||||
|
|
||||||
public get parent(): TreeNode {
|
public get parent(): TreeNode {
|
||||||
return this._parent;
|
return this._parent;
|
||||||
}
|
}
|
||||||
@@ -77,8 +78,13 @@ export abstract class TreeNode implements ITreeNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async updateFileSource(connection: SqlClusterConnection): Promise<void> {
|
public async updateFileSource(connection: SqlClusterConnection): Promise<void> {
|
||||||
this.fileSource = await connection.createHdfsFileSource();
|
this._fileSource = await connection.createHdfsFileSource();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getFileSource(): Promise<IFileSource | undefined> {
|
||||||
|
return this._fileSource;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The value to use for this node in the node path
|
* The value to use for this node in the node path
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export class OpenSparkJobSubmissionDialogCommand extends Command {
|
|||||||
try {
|
try {
|
||||||
let sqlClusterConnection: SqlClusterConnection = undefined;
|
let sqlClusterConnection: SqlClusterConnection = undefined;
|
||||||
if (context.type === constants.ObjectExplorerService) {
|
if (context.type === constants.ObjectExplorerService) {
|
||||||
sqlClusterConnection = SqlClusterLookUp.findSqlClusterConnection(context, this.appContext);
|
sqlClusterConnection = await SqlClusterLookUp.findSqlClusterConnection(context, this.appContext);
|
||||||
}
|
}
|
||||||
if (!sqlClusterConnection) {
|
if (!sqlClusterConnection) {
|
||||||
sqlClusterConnection = await this.selectConnection();
|
sqlClusterConnection = await this.selectConnection();
|
||||||
@@ -103,7 +103,7 @@ export class OpenSparkJobSubmissionDialogCommand extends Command {
|
|||||||
let sqlConnection = connectionMap.get(selectedHost);
|
let sqlConnection = connectionMap.get(selectedHost);
|
||||||
if (!sqlConnection) { throw new Error(errorMsg); }
|
if (!sqlConnection) { throw new Error(errorMsg); }
|
||||||
|
|
||||||
let sqlClusterConnection = await SqlClusterLookUp.getSqlClusterConnection(sqlConnection);
|
let sqlClusterConnection = await SqlClusterLookUp.getSqlClusterConnectionParams(sqlConnection, this.appContext);
|
||||||
if (!sqlClusterConnection) {
|
if (!sqlClusterConnection) {
|
||||||
throw new Error(localize('errorNotSqlBigDataCluster', "The selected server does not belong to a SQL Server Big Data Cluster"));
|
throw new Error(localize('errorNotSqlBigDataCluster', "The selected server does not belong to a SQL Server Big Data Cluster"));
|
||||||
}
|
}
|
||||||
@@ -159,7 +159,7 @@ export class OpenSparkJobSubmissionDialogTask {
|
|||||||
|
|
||||||
async execute(profile: azdata.IConnectionProfile, ...args: any[]): Promise<void> {
|
async execute(profile: azdata.IConnectionProfile, ...args: any[]): Promise<void> {
|
||||||
try {
|
try {
|
||||||
let sqlClusterConnection = SqlClusterLookUp.findSqlClusterConnection(profile, this.appContext);
|
let sqlClusterConnection = await SqlClusterLookUp.findSqlClusterConnection(profile, this.appContext);
|
||||||
if (!sqlClusterConnection) {
|
if (!sqlClusterConnection) {
|
||||||
throw new Error(LocalizedConstants.sparkJobSubmissionNoSqlBigDataClusterFound);
|
throw new Error(LocalizedConstants.sparkJobSubmissionNoSqlBigDataClusterFound);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export class OpenSparkYarnHistoryTask {
|
|||||||
|
|
||||||
async execute(sqlConnProfile: azdata.IConnectionProfile, isSpark: boolean): Promise<void> {
|
async execute(sqlConnProfile: azdata.IConnectionProfile, isSpark: boolean): Promise<void> {
|
||||||
try {
|
try {
|
||||||
let sqlClusterConnection = SqlClusterLookUp.findSqlClusterConnection(sqlConnProfile, this.appContext);
|
let sqlClusterConnection = await SqlClusterLookUp.findSqlClusterConnection(sqlConnProfile, this.appContext);
|
||||||
if (!sqlClusterConnection) {
|
if (!sqlClusterConnection) {
|
||||||
let name = isSpark ? 'Spark' : 'Yarn';
|
let name = isSpark ? 'Spark' : 'Yarn';
|
||||||
vscode.window.showErrorMessage(loc.sparkConnectionRequired(name));
|
vscode.window.showErrorMessage(loc.sparkConnectionRequired(name));
|
||||||
|
|||||||
@@ -11,14 +11,22 @@ import * as UUID from 'vscode-languageclient/lib/utils/uuid';
|
|||||||
import { AppContext } from './appContext';
|
import { AppContext } from './appContext';
|
||||||
import { SqlClusterConnection } from './objectExplorerNodeProvider/connection';
|
import { SqlClusterConnection } from './objectExplorerNodeProvider/connection';
|
||||||
import { ICommandObjectExplorerContext } from './objectExplorerNodeProvider/command';
|
import { ICommandObjectExplorerContext } from './objectExplorerNodeProvider/command';
|
||||||
import { IEndpoint, getClusterEndpoints, getHostAndPortFromEndpoint } from './utils';
|
import { getClusterEndpoints, getHostAndPortFromEndpoint } from './utils';
|
||||||
import { MssqlObjectExplorerNodeProvider } from './objectExplorerNodeProvider/objectExplorerNodeProvider';
|
import { MssqlObjectExplorerNodeProvider } from './objectExplorerNodeProvider/objectExplorerNodeProvider';
|
||||||
|
import CodeAdapter from './prompts/adapter';
|
||||||
|
import { IQuestion, QuestionTypes } from './prompts/question';
|
||||||
|
import * as nls from 'vscode-nls';
|
||||||
|
import { AuthType } from './util/auth';
|
||||||
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
export function findSqlClusterConnection(
|
export async function findSqlClusterConnection(
|
||||||
obj: ICommandObjectExplorerContext | azdata.IConnectionProfile,
|
obj: ICommandObjectExplorerContext | azdata.IConnectionProfile,
|
||||||
appContext: AppContext): SqlClusterConnection {
|
appContext: AppContext): Promise<SqlClusterConnection | undefined> {
|
||||||
|
|
||||||
if (!obj || !appContext) { return undefined; }
|
if (!obj || !appContext) {
|
||||||
|
console.error('SqlClusterLookup::findSqlClusterConnection - No context available');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
let sqlConnProfile: azdata.IConnectionProfile;
|
let sqlConnProfile: azdata.IConnectionProfile;
|
||||||
if ('type' in obj && obj.type === constants.ObjectExplorerService
|
if ('type' in obj && obj.type === constants.ObjectExplorerService
|
||||||
@@ -30,25 +38,37 @@ export function findSqlClusterConnection(
|
|||||||
|
|
||||||
let sqlClusterConnection: SqlClusterConnection = undefined;
|
let sqlClusterConnection: SqlClusterConnection = undefined;
|
||||||
if (sqlConnProfile) {
|
if (sqlConnProfile) {
|
||||||
sqlClusterConnection = findSqlClusterConnectionBySqlConnProfile(sqlConnProfile, appContext);
|
sqlClusterConnection = await findSqlClusterConnectionBySqlConnProfile(sqlConnProfile, appContext);
|
||||||
|
} else {
|
||||||
|
console.error('SqlClusterLookup::findSqlClusterConnection - No connection profile');
|
||||||
}
|
}
|
||||||
return sqlClusterConnection;
|
return sqlClusterConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findSqlClusterConnectionBySqlConnProfile(sqlConnProfile: azdata.IConnectionProfile, appContext: AppContext): SqlClusterConnection {
|
async function findSqlClusterConnectionBySqlConnProfile(sqlConnProfile: azdata.IConnectionProfile, appContext: AppContext): Promise<SqlClusterConnection | undefined> {
|
||||||
if (!sqlConnProfile || !appContext) { return undefined; }
|
if (!sqlConnProfile || !appContext) {
|
||||||
|
console.error('SqlClusterLookup::findSqlClusterConnectionBySqlConnProfile - No context available');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
let sqlOeNodeProvider = appContext.getService<MssqlObjectExplorerNodeProvider>(constants.ObjectExplorerService);
|
let sqlOeNodeProvider = appContext.getService<MssqlObjectExplorerNodeProvider>(constants.ObjectExplorerService);
|
||||||
if (!sqlOeNodeProvider) { return undefined; }
|
if (!sqlOeNodeProvider) {
|
||||||
|
console.error('SqlClusterLookup::findSqlClusterConnectionBySqlConnProfile - No OE Node Provider available');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
let sqlClusterSession = sqlOeNodeProvider.findSqlClusterSessionBySqlConnProfile(sqlConnProfile);
|
let sqlClusterSession = sqlOeNodeProvider.findSqlClusterSessionBySqlConnProfile(sqlConnProfile);
|
||||||
if (!sqlClusterSession) { return undefined; }
|
if (!sqlClusterSession) {
|
||||||
|
console.error('SqlClusterLookup::findSqlClusterConnectionBySqlConnProfile - No SQL Cluster Session found');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
return sqlClusterSession.sqlClusterConnection;
|
return sqlClusterSession.getSqlClusterConnection();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSqlClusterConnection(
|
export async function getSqlClusterConnectionParams(
|
||||||
obj: azdata.IConnectionProfile | azdata.connection.Connection | ICommandObjectExplorerContext): Promise<ConnectionParam> {
|
obj: azdata.IConnectionProfile | azdata.connection.Connection | ICommandObjectExplorerContext,
|
||||||
|
appContext: AppContext): Promise<ConnectionParam> {
|
||||||
|
|
||||||
if (!obj) { return undefined; }
|
if (!obj) { return undefined; }
|
||||||
|
|
||||||
@@ -57,16 +77,16 @@ export async function getSqlClusterConnection(
|
|||||||
if (obj.providerName === constants.mssqlClusterProviderName) {
|
if (obj.providerName === constants.mssqlClusterProviderName) {
|
||||||
sqlClusterConnInfo = 'id' in obj ? connProfileToConnectionParam(obj) : connToConnectionParam(obj);
|
sqlClusterConnInfo = 'id' in obj ? connProfileToConnectionParam(obj) : connToConnectionParam(obj);
|
||||||
} else {
|
} else {
|
||||||
sqlClusterConnInfo = await createSqlClusterConnInfo(obj);
|
sqlClusterConnInfo = await createSqlClusterConnInfo(obj, appContext);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sqlClusterConnInfo = await createSqlClusterConnInfo(obj.explorerContext.connectionProfile);
|
sqlClusterConnInfo = await createSqlClusterConnInfo(obj.explorerContext.connectionProfile, appContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
return sqlClusterConnInfo;
|
return sqlClusterConnInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createSqlClusterConnInfo(sqlConnInfo: azdata.IConnectionProfile | azdata.connection.Connection): Promise<ConnectionParam> {
|
async function createSqlClusterConnInfo(sqlConnInfo: azdata.IConnectionProfile | azdata.connection.Connection, appContext: AppContext): Promise<ConnectionParam> {
|
||||||
if (!sqlConnInfo) { return undefined; }
|
if (!sqlConnInfo) { return undefined; }
|
||||||
|
|
||||||
let connectionId: string = 'id' in sqlConnInfo ? sqlConnInfo.id : sqlConnInfo.connectionId;
|
let connectionId: string = 'id' in sqlConnInfo ? sqlConnInfo.id : sqlConnInfo.connectionId;
|
||||||
@@ -75,12 +95,9 @@ async function createSqlClusterConnInfo(sqlConnInfo: azdata.IConnectionProfile |
|
|||||||
let serverInfo = await azdata.connection.getServerInfo(connectionId);
|
let serverInfo = await azdata.connection.getServerInfo(connectionId);
|
||||||
if (!serverInfo || !serverInfo.options) { return undefined; }
|
if (!serverInfo || !serverInfo.options) { return undefined; }
|
||||||
|
|
||||||
let endpoints: IEndpoint[] = getClusterEndpoints(serverInfo);
|
let endpoints: bdc.IEndpointModel[] = getClusterEndpoints(serverInfo);
|
||||||
if (!endpoints || endpoints.length === 0) { return undefined; }
|
if (!endpoints || endpoints.length === 0) { return undefined; }
|
||||||
|
|
||||||
let index = endpoints.findIndex(ep => ep.serviceName.toLowerCase() === constants.hadoopEndpointNameGateway.toLowerCase());
|
|
||||||
if (index < 0) { return undefined; }
|
|
||||||
|
|
||||||
let credentials = await azdata.connection.getCredentials(connectionId);
|
let credentials = await azdata.connection.getCredentials(connectionId);
|
||||||
if (!credentials) { return undefined; }
|
if (!credentials) { return undefined; }
|
||||||
|
|
||||||
@@ -90,29 +107,99 @@ async function createSqlClusterConnInfo(sqlConnInfo: azdata.IConnectionProfile |
|
|||||||
options: {}
|
options: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
let hostAndIp = getHostAndPortFromEndpoint(endpoints[index].endpoint);
|
let clusterController: bdc.IClusterController | undefined = undefined;
|
||||||
|
let authType = clusterConnInfo.options[constants.authenticationTypePropName] = sqlConnInfo.options[constants.authenticationTypePropName];
|
||||||
|
const controllerEndpoint = endpoints.find(ep => ep.name.toLowerCase() === 'controller');
|
||||||
|
if (authType && authType.toLowerCase() !== constants.integratedAuth) {
|
||||||
|
const usernameKey = `bdc.username::${connectionId}`;
|
||||||
|
const savedUsername = appContext.extensionContext.globalState.get(usernameKey);
|
||||||
|
const credentialProvider = await azdata.credentials.getProvider('mssql.bdc.password');
|
||||||
|
const savedPassword = (await credentialProvider.readCredential(connectionId)).password;
|
||||||
|
// If we don't have a previously saved username/password then use the SQL connection credentials as a best guess,
|
||||||
|
// if those don't work then we'll prompt the user for the info
|
||||||
|
clusterConnInfo.options[constants.userPropName] = savedUsername ?? sqlConnInfo.options[constants.userPropName];
|
||||||
|
clusterConnInfo.options[constants.passwordPropName] = savedPassword ?? credentials.password;
|
||||||
|
try {
|
||||||
|
clusterController = await getClusterController(controllerEndpoint.endpoint, clusterConnInfo);
|
||||||
|
// We've successfully connected so now store the username/password for future connections
|
||||||
|
appContext.extensionContext.globalState.update(usernameKey, clusterConnInfo.options[constants.userPropName]);
|
||||||
|
credentialProvider.saveCredential(connectionId, clusterConnInfo.options[constants.passwordPropName]);
|
||||||
|
clusterConnInfo.options[constants.userPropName] = await clusterController.getKnoxUsername(clusterConnInfo.options[constants.userPropName]);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`Unexpected error getting Knox username for SQL Cluster connection: ${err}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
clusterController = await getClusterController(controllerEndpoint.endpoint, clusterConnInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
let hadoopEndpointIndex = endpoints.findIndex(ep => ep.name.toLowerCase() === constants.hadoopEndpointNameGateway.toLowerCase());
|
||||||
|
if (hadoopEndpointIndex < 0) {
|
||||||
|
endpoints = (await clusterController.getEndPoints()).endPoints;
|
||||||
|
hadoopEndpointIndex = endpoints.findIndex(ep => ep.name.toLowerCase() === constants.hadoopEndpointNameGateway.toLowerCase());
|
||||||
|
}
|
||||||
|
const hostAndIp = getHostAndPortFromEndpoint(endpoints[hadoopEndpointIndex].endpoint);
|
||||||
clusterConnInfo.options[constants.hostPropName] = hostAndIp.host;
|
clusterConnInfo.options[constants.hostPropName] = hostAndIp.host;
|
||||||
// TODO should we default the port? Or just ignore later?
|
// TODO should we default the port? Or just ignore later?
|
||||||
clusterConnInfo.options[constants.knoxPortPropName] = hostAndIp.port || constants.defaultKnoxPort;
|
clusterConnInfo.options[constants.knoxPortPropName] = hostAndIp.port || constants.defaultKnoxPort;
|
||||||
let authType = clusterConnInfo.options[constants.authenticationTypePropName] = sqlConnInfo.options[constants.authenticationTypePropName];
|
|
||||||
if (authType && authType.toLowerCase() !== constants.integratedAuth) {
|
|
||||||
clusterConnInfo.options[constants.userPropName] = sqlConnInfo.options[constants.userPropName]; //should be the same user as sql master
|
|
||||||
clusterConnInfo.options[constants.passwordPropName] = credentials.password;
|
|
||||||
try {
|
|
||||||
const bdcApi = <bdc.IExtension>await vscode.extensions.getExtension(bdc.constants.extensionName).activate();
|
|
||||||
const controllerEndpoint = endpoints.find(ep => ep.serviceName.toLowerCase() === 'controller');
|
|
||||||
const controller = bdcApi.getClusterController(controllerEndpoint.endpoint, 'basic', sqlConnInfo.options[constants.userPropName], credentials.password);
|
|
||||||
clusterConnInfo.options[constants.userPropName] = await controller.getKnoxUsername(sqlConnInfo.options[constants.userPropName]);
|
|
||||||
} catch (err) {
|
|
||||||
console.log(`Unexpected error getting Knox username for SQL Cluster connection: ${err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
clusterConnInfo = connToConnectionParam(clusterConnInfo);
|
clusterConnInfo = connToConnectionParam(clusterConnInfo);
|
||||||
|
|
||||||
return clusterConnInfo;
|
return clusterConnInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getClusterController(controllerEndpoint: string, connInfo: ConnectionParam): Promise<bdc.IClusterController | undefined> {
|
||||||
|
const bdcApi = <bdc.IExtension>await vscode.extensions.getExtension(bdc.constants.extensionName).activate();
|
||||||
|
let authType: bdc.AuthType = connInfo.options[constants.authenticationTypePropName].toLowerCase() === AuthType.Integrated ? 'integrated' : 'basic';
|
||||||
|
const controller = bdcApi.getClusterController(
|
||||||
|
controllerEndpoint,
|
||||||
|
authType,
|
||||||
|
connInfo.options[constants.userPropName],
|
||||||
|
connInfo.options[constants.passwordPropName]);
|
||||||
|
try {
|
||||||
|
await controller.getClusterConfig();
|
||||||
|
return controller;
|
||||||
|
} catch (err) {
|
||||||
|
// Initial username/password failed so prompt user for username password until either user
|
||||||
|
// cancels out or we successfully connect
|
||||||
|
console.log(`Error connecting to cluster controller: ${err}`);
|
||||||
|
let errorMessage = '';
|
||||||
|
while (true) {
|
||||||
|
const prompter = new CodeAdapter();
|
||||||
|
let username = await prompter.promptSingle<string>(<IQuestion>{
|
||||||
|
type: QuestionTypes.input,
|
||||||
|
name: 'inputPrompt',
|
||||||
|
message: localize('promptBDCUsername', "{0}Please provide the username to connect to the BDC Controller:", errorMessage),
|
||||||
|
default: connInfo.options[constants.userPropName]
|
||||||
|
});
|
||||||
|
if (!username) {
|
||||||
|
console.log(`User cancelled out of username prompt for BDC Controller`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const password = await prompter.promptSingle<string>(<IQuestion>{
|
||||||
|
type: QuestionTypes.password,
|
||||||
|
name: 'passwordPrompt',
|
||||||
|
message: localize('promptBDCPassword', "Please provide the password to connect to the BDC Controller"),
|
||||||
|
default: ''
|
||||||
|
});
|
||||||
|
if (!password) {
|
||||||
|
console.log(`User cancelled out of password prompt for BDC Controller`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const controller = bdcApi.getClusterController(controllerEndpoint, authType, username, password);
|
||||||
|
try {
|
||||||
|
await controller.getClusterConfig();
|
||||||
|
// Update our connection with the new info
|
||||||
|
connInfo.options[constants.userPropName] = username;
|
||||||
|
connInfo.options[constants.passwordPropName] = password;
|
||||||
|
return controller;
|
||||||
|
} catch (err) {
|
||||||
|
errorMessage = localize('bdcConnectError', "Error: {0}. ", err.message ?? err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(localize('usernameAndPasswordRequired', "Username and password are required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
function connProfileToConnectionParam(connectionProfile: azdata.IConnectionProfile): ConnectionParam {
|
function connProfileToConnectionParam(connectionProfile: azdata.IConnectionProfile): ConnectionParam {
|
||||||
let result = Object.assign(connectionProfile, { connectionId: connectionProfile.id });
|
let result = Object.assign(connectionProfile, { connectionId: connectionProfile.id });
|
||||||
return <ConnectionParam>result;
|
return <ConnectionParam>result;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import * as azdata from 'azdata';
|
import * as azdata from 'azdata';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
|
import * as bdc from 'bdc';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
@@ -222,15 +223,15 @@ export function getUserHome(): string {
|
|||||||
return process.env.HOME || process.env.USERPROFILE;
|
return process.env.HOME || process.env.USERPROFILE;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getClusterEndpoints(serverInfo: azdata.ServerInfo): IEndpoint[] | undefined {
|
export function getClusterEndpoints(serverInfo: azdata.ServerInfo): bdc.IEndpointModel[] | undefined {
|
||||||
let endpoints: RawEndpoint[] = serverInfo.options[constants.clusterEndpointsProperty];
|
let endpoints: RawEndpoint[] = serverInfo.options[constants.clusterEndpointsProperty];
|
||||||
if (!endpoints || endpoints.length === 0) { return []; }
|
if (!endpoints || endpoints.length === 0) { return []; }
|
||||||
|
|
||||||
return endpoints.map(e => {
|
return endpoints.map(e => {
|
||||||
// If endpoint is missing, we're on CTP bits. All endpoints from the CTP serverInfo should be treated as HTTPS
|
// If endpoint is missing, we're on CTP bits. All endpoints from the CTP serverInfo should be treated as HTTPS
|
||||||
let endpoint = e.endpoint ? e.endpoint : `https://${e.ipAddress}:${e.port}`;
|
let endpoint = e.endpoint ? e.endpoint : `https://${e.ipAddress}:${e.port}`;
|
||||||
let updatedEndpoint: IEndpoint = {
|
let updatedEndpoint: bdc.IEndpointModel = {
|
||||||
serviceName: e.serviceName,
|
name: e.serviceName,
|
||||||
description: e.description,
|
description: e.description,
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
protocol: e.protocol
|
protocol: e.protocol
|
||||||
@@ -239,6 +240,12 @@ export function getClusterEndpoints(serverInfo: azdata.ServerInfo): IEndpoint[]
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function isBigDataCluster(connectionId: string): Promise<boolean> {
|
||||||
|
const serverInfo = await azdata.connection.getServerInfo(connectionId);
|
||||||
|
|
||||||
|
return !!serverInfo?.options?.[constants.isBigDataClusterProperty];
|
||||||
|
}
|
||||||
|
|
||||||
export type HostAndIp = { host: string, port: string };
|
export type HostAndIp = { host: string, port: string };
|
||||||
|
|
||||||
export function getHostAndPortFromEndpoint(endpoint: string): HostAndIp {
|
export function getHostAndPortFromEndpoint(endpoint: string): HostAndIp {
|
||||||
@@ -266,13 +273,6 @@ interface RawEndpoint {
|
|||||||
port?: number;
|
port?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IEndpoint {
|
|
||||||
serviceName: string;
|
|
||||||
description: string;
|
|
||||||
endpoint: string;
|
|
||||||
protocol: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isValidNumber(maybeNumber: any) {
|
export function isValidNumber(maybeNumber: any) {
|
||||||
return maybeNumber !== undefined
|
return maybeNumber !== undefined
|
||||||
&& maybeNumber !== null
|
&& maybeNumber !== null
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
"default": [],
|
"default": [],
|
||||||
"description": "%notebook.pinnedNotebooks.description%",
|
"description": "%notebook.pinnedNotebooks.description%",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"type": "object"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user