Compare commits

..

10 Commits

Author SHA1 Message Date
Charles Gagnon
765c7064d4 Add Product Feedback survey link to welcome page (#14705) (#14739)
* Fix welcome page

* Add link
2021-03-15 17:25:10 -07:00
Karl Burtram
dfeb80ed3e Fix script loading in sandbox windows (#14727) (#14731)
* Fix script loading in sandbox windows

* Avoid uglify error
2021-03-15 13:49:23 -07:00
Charles Gagnon
e2ce6bebac Fix filtered resource options persisting across deployments (#14702) (#14711) 2021-03-15 12:06:54 -07:00
Charles Gagnon
3368af9371 Fix welcome page (#14701) (#14706) 2021-03-14 22:27:42 -07:00
Charles Gagnon
26156b4e61 Disable auto sync service (#14677) (#14689)
* Disable auto sync service

* skip tests

(cherry picked from commit 1c671676bf)
2021-03-12 10:49:21 -08:00
Hale Rankin
cbbfdb98db Manage Packages icon position fix (#14528) (#14679)
* Added CSS to treat manage packages icon position.

* Wired up a rough concept showing how I propose to pass a masked-icon class, along with the icon name - derived from the action icon path - to the injected action prior to rendering. The end result is an injected action with an icon which behaves like the others in the notebook toolbar.

* Revert "Added CSS to treat manage packages icon position."

This reverts commit 215a67244a78224fe8fd2e6480b7e50d37a15dc0.

* Revert "Wired up a rough concept showing how I propose to pass a masked-icon class, along with the icon name - derived from the action icon path - to the injected action prior to rendering. The end result is an injected action with an icon which behaves like the others in the notebook toolbar."

This reverts commit 1e0cf116602192aa554334d564e855167b0e8bb6.

* Modified LabeledMenuItemActionItem to add masked-icon class to injected label element.

* Modified LabeledMenuItemActionItem to add masked-icon class and styles for injected label element.

* To prevent conflict with Dashboard, I duplicated the existing LabelMenuItemActionItem and odified it.

* Added comment to new method. Added non-prefixed mask-image style.

* Renamed method and modified comments.
2021-03-12 09:11:26 -08:00
Barbara Valdez
9ae254c91e Queue concurrent calls when initializing contents in BookModel (#14586) (#14619)
* Initial work for handling concurrent operations when calling initialized books

* fixes to init contents

* create a queue of deferred promises for initializing books

* resolve active promise and set to undefined

* remove duplicated variable

* address pr comments
2021-03-10 19:04:20 -08:00
Alan Ren
4772be5dcc handle query gallery with tags (#14633) (#14636)
* handle query gallery with tags

* fix typo
2021-03-10 16:10:42 -08:00
Aasim Khan
41e756b906 Fixing welcome page tour. (#14640) (#14661)
* Fixing the icon names

* Fixing settings card position

* getting icon selectors from their source

* Changing var name from dataExplorer to dataExplorerId
2021-03-10 16:09:06 -08:00
Charles Gagnon
e2bd65cdb0 Change tenant to tenant.id when passing to azdata.accounts.getAccountSecurityToken() method. (#14588) (#14605)
(cherry picked from commit 0bea923c0a)

Co-authored-by: Jeff Trimmer <jetrimme@microsoft.com>
2021-03-10 12:03:27 -08:00
1694 changed files with 171266 additions and 279410 deletions

View File

@@ -1,20 +1,5 @@
# Change Log
## Version 1.27.0
* Release date: March 17, 2021
* Release status: General Availability
* New Notebook Features:
* Added create book dialog
* Extension Updates:
* Import
* Dacpac
* Machine Learning
* SQL Assessment
* Arc
* SQL Database Projects
* ASDE Deployment
* Bux Fixes
## Version 1.26.1
* Release date: February 25, 2021
* Release status: General Availability

View File

@@ -131,10 +131,10 @@ Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the [Source EULA](LICENSE.txt).
[win-user]: https://go.microsoft.com/fwlink/?linkid=2157460
[win-system]: https://go.microsoft.com/fwlink/?linkid=2157459
[win-zip]: https://go.microsoft.com/fwlink/?linkid=2157458
[osx-zip]: https://go.microsoft.com/fwlink/?linkid=2157456
[linux-zip]: https://go.microsoft.com/fwlink/?linkid=2157353
[linux-rpm]: https://go.microsoft.com/fwlink/?linkid=2157248
[linux-deb]: https://go.microsoft.com/fwlink/?linkid=2157352
[win-user]: https://go.microsoft.com/fwlink/?linkid=2154985
[win-system]: https://go.microsoft.com/fwlink/?linkid=2155159
[win-zip]: https://go.microsoft.com/fwlink/?linkid=2155221
[osx-zip]: https://go.microsoft.com/fwlink/?linkid=2155096
[linux-zip]: https://go.microsoft.com/fwlink/?linkid=2154986
[linux-rpm]: https://go.microsoft.com/fwlink/?linkid=2155222
[linux-deb]: https://go.microsoft.com/fwlink/?linkid=2155223

View File

@@ -12,7 +12,7 @@ expressly granted herein, whether by implication, estoppel or otherwise.
angular2-grid: https://github.com/BTMorton/angular2-grid
angular2-slickgrid: https://github.com/Microsoft/angular2-slickgrid
applicationinsights: https://github.com/Microsoft/ApplicationInsights-node.js
axios: https://github.com/axios/axios
axios: https://github.com/axios/axios
bootstrap: https://github.com/twbs/bootstrap
chart.js: https://github.com/Timer/chartjs
chokidar: https://github.com/paulmillr/chokidar
@@ -39,7 +39,7 @@ expressly granted herein, whether by implication, estoppel or otherwise.
jschardet: https://github.com/aadsm/jschardet
jupyter-powershell: https://github.com/vors/jupyter-powershell
JupyterLab: https://github.com/jupyterlab/jupyterlab
keytar: https://github.com/atom/node-keytar
keytar: https://github.com/atom/node-keytar
make-error: https://github.com/JsCommunity/make-error
mark.js: https://github.com/julmot/mark.js
minimist: https://github.com/substack/minimist
@@ -54,8 +54,7 @@ expressly granted herein, whether by implication, estoppel or otherwise.
primeng: https://github.com/primefaces/primeng
process-nextick-args: https://github.com/calvinmetcalf/process-nextick-args
pty.js: https://github.com/chjj/pty.js
pyzmq: https://github.com/zeromq/pyzmq
qs: https://github.com/ljharb/qs
qs: https://github.com/ljharb/qs
reflect-metadata: https://github.com/rbuckton/reflect-metadata
request: https://github.com/request/request
rxjs: https://github.com/ReactiveX/RxJS
@@ -1549,40 +1548,6 @@ THE SOFTWARE.
=========================================
END OF pty.js NOTICES AND INFORMATION
%% PyZMQ NOTICES AND INFORMATION BEGIN HERE
=========================================
Copyright (c) 2009-2012, Brian Granger, Min Ragan-Kelley
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
Neither the name of PyZMQ nor the names of its contributors may be used to
endorse or promote products derived from this software without specific prior
written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
=========================================
END OF pyzmq NOTICES AND INFORMATION
%% reflect-metadata NOTICES AND INFORMATION BEGIN HERE
=========================================
Apache License

View File

@@ -151,7 +151,7 @@ steps:
inputs:
ConnectedServiceName: 'Code Signing'
FolderPath: '$(agent.builddirectory)/azuredatastudio-win32-x64'
Pattern: '*.exe,*.node,resources/app/node_modules.asar.unpacked/*.dll,swiftshader/*.dll,d3dcompiler_47.dll,vulkan-1.dll,libGLESv2.dll,ffmpeg.dll,libEGL.dll,Microsoft.SqlTools.Hosting.dll,Microsoft.SqlTools.ResourceProvider.Core.dll,Microsoft.SqlTools.ResourceProvider.DefaultImpl.dll,MicrosoftSqlToolsCredentials.dll,MicrosoftSqlToolsServiceLayer.dll,Newtonsoft.Json.dll,SqlSerializationService.dll,SqlToolsResourceProviderService.dll,Microsoft.SqlServer.*.dll,Microsoft.Data.Tools.Sql.BatchParser.dll'
Pattern: '*.exe,*.node,resources/app/node_modules.asar.unpacked/*.dll,swiftshader/*.dll,d3dcompiler_47.dll,libGLESv2.dll,ffmpeg.dll,libEGL.dll,Microsoft.SqlTools.Hosting.dll,Microsoft.SqlTools.ResourceProvider.Core.dll,Microsoft.SqlTools.ResourceProvider.DefaultImpl.dll,MicrosoftSqlToolsCredentials.dll,MicrosoftSqlToolsServiceLayer.dll,Newtonsoft.Json.dll,SqlSerializationService.dll,SqlToolsResourceProviderService.dll,Microsoft.SqlServer.*.dll,Microsoft.Data.Tools.Sql.BatchParser.dll'
signConfigType: inlineSignParams
inlineOperation: |
[

View File

@@ -114,8 +114,6 @@ const indentationFilter = [
'!extensions/big-data-cluster/src/bigDataCluster/controller/apiGenerated.ts',
'!extensions/big-data-cluster/src/bigDataCluster/controller/clusterApiGenerated2.ts',
'!resources/linux/snap/electron-launch',
'!extensions/markdown-language-features/media/*.js',
'!extensions/simple-browser/media/*.js',
'!resources/xlf/LocProject.json', // {{SQL CARBON EDIT}}
'!build/**/*' // {{SQL CARBON EDIT}}
];

View File

@@ -3751,9 +3751,9 @@ xtend@~4.0.1:
integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68=
y18n@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4"
integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==
version "4.0.0"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
yallist@^4.0.0:
version "4.0.0"

View File

@@ -1 +1,2 @@
title: Azure Arc Data Services
title: Azure Arc Data Services
description: A collection of notebooks to support Azure Arc Data Services.

View File

@@ -1,10 +1,12 @@
- title: Welcome
url: /readme
not_numbered: true
- title: Search
search: true
- title: Postgres
url: /postgres/readme
not_numbered: true
expand_sections: true
sections:
- title: Postgres
url: /postgres/readme
not_numbered: true
sections:
- title: TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter
url: postgres/tsg100-troubleshoot-postgres
- title: TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter
url: postgres/tsg100-troubleshoot-postgres

View File

@@ -2,10 +2,6 @@
- This chapter contains notebooks for troubleshooting Postgres on Azure Arc
[Home](../readme.md)
## Notebooks in this Chapter
- [TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter](tsg100-troubleshoot-postgres.ipynb)
- [TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter](../postgres/tsg100-troubleshoot-postgres.ipynb)

View File

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

View File

@@ -2,11 +2,7 @@
"cells": [
{
"cell_type": "markdown",
"execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"metadata": {},
"source": [
"TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter\n",
"===================================================================\n",
@@ -39,17 +35,14 @@
"# the user will be prompted to select a server.\n",
"namespace = os.environ.get('POSTGRES_SERVER_NAMESPACE')\n",
"name = os.environ.get('POSTGRES_SERVER_NAME')\n",
"version = os.environ.get('POSTGRES_SERVER_VERSION')\n",
"\n",
"tail_lines = 50"
]
},
{
"cell_type": "markdown",
"execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"metadata": {},
"source": [
"### Common functions\n",
"\n",
@@ -70,6 +63,7 @@
"import sys\n",
"import os\n",
"import re\n",
"import json\n",
"import platform\n",
"import shlex\n",
"import shutil\n",
@@ -82,7 +76,11 @@
"error_hints = {} # Output in stderr where a known SOP/TSG exists which will be HINTed for further help\n",
"install_hint = {} # The SOP to help install the executable if it cannot be found\n",
"\n",
"def run(cmd, return_output=False, no_output=False, retry_count=0, base64_decode=False, return_as_json=False):\n",
"first_run = True\n",
"rules = None\n",
"debug_logging = False\n",
"\n",
"def run(cmd, return_output=False, no_output=False, retry_count=0):\n",
" \"\"\"Run shell command, stream stdout, print stderr and optionally return output\n",
"\n",
" NOTES:\n",
@@ -105,6 +103,13 @@
" output = \"\"\n",
" retry = False\n",
"\n",
" global first_run\n",
" global rules\n",
"\n",
" if first_run:\n",
" first_run = False\n",
" rules = load_rules()\n",
"\n",
" # When running `azdata sql query` on Windows, replace any \\n in \"\"\" strings, with \" \", otherwise we see:\n",
" #\n",
" # ('HY090', '[HY090] [Microsoft][ODBC Driver Manager] Invalid string or buffer length (0) (SQLExecDirectW)')\n",
@@ -167,12 +172,7 @@
" if which_binary == None:\n",
" which_binary = shutil.which(cmd_actual[0])\n",
"\n",
" # Display an install HINT, so the user can click on a SOP to install the missing binary\n",
" #\n",
" if which_binary == None:\n",
" print(f\"The path used to search for '{cmd_actual[0]}' was:\")\n",
" print(sys.path)\n",
"\n",
" if user_provided_exe_name in install_hint and install_hint[user_provided_exe_name] is not None:\n",
" display(Markdown(f'HINT: Use [{install_hint[user_provided_exe_name][0]}]({install_hint[user_provided_exe_name][1]}) to resolve this issue.'))\n",
"\n",
@@ -219,6 +219,8 @@
" break # otherwise infinite hang, have not worked out why yet.\n",
" else:\n",
" print(line, end='')\n",
" if rules is not None:\n",
" apply_expert_rules(line)\n",
"\n",
" if wait:\n",
" p.wait()\n",
@@ -274,22 +276,25 @@
" if line_decoded.find(error_hint[0]) != -1:\n",
" display(Markdown(f'HINT: Use [{error_hint[1]}]({error_hint[2]}) to resolve this issue.'))\n",
"\n",
" # apply expert rules (to run follow-on notebooks), based on output\n",
" #\n",
" if rules is not None:\n",
" apply_expert_rules(line_decoded)\n",
"\n",
" # Verify if a transient error, if so automatically retry (recursive)\n",
" #\n",
" if user_provided_exe_name in retry_hints:\n",
" for retry_hint in retry_hints[user_provided_exe_name]:\n",
" if line_decoded.find(retry_hint) != -1:\n",
" if retry_count \u003c MAX_RETRIES:\n",
" if retry_count < MAX_RETRIES:\n",
" print(f\"RETRY: {retry_count} (due to: {retry_hint})\")\n",
" retry_count = retry_count + 1\n",
" output = run(cmd, return_output=return_output, retry_count=retry_count)\n",
"\n",
" if return_output:\n",
" if base64_decode:\n",
" import base64\n",
" return base64.b64decode(output).decode('utf-8')\n",
" else:\n",
" return output\n",
" return output\n",
" else:\n",
" return\n",
"\n",
" elapsed = datetime.datetime.now().replace(microsecond=0) - start_time\n",
"\n",
@@ -306,31 +311,78 @@
" print(f'\\nSUCCESS: {elapsed}s elapsed.\\n')\n",
"\n",
" if return_output:\n",
" if base64_decode:\n",
" import base64\n",
" return base64.b64decode(output).decode('utf-8')\n",
" else:\n",
" return output\n",
" return output\n",
"\n",
"def load_json(filename):\n",
" \"\"\"Load a json file from disk and return the contents\"\"\"\n",
"\n",
" with open(filename, encoding=\"utf8\") as json_file:\n",
" return json.load(json_file)\n",
"\n",
"def load_rules():\n",
" \"\"\"Load any 'expert rules' from the metadata of this notebook (.ipynb) that should be applied to the stderr of the running executable\"\"\"\n",
"\n",
" # Load this notebook as json to get access to the expert rules in the notebook metadata.\n",
" #\n",
" try:\n",
" j = load_json(\"tsg100-troubleshoot-postgres.ipynb\")\n",
" except:\n",
" pass # If the user has renamed the book, we can't load ourself. NOTE: Is there a way in Jupyter, to know your own filename?\n",
" else:\n",
" if \"metadata\" in j and \\\n",
" \"azdata\" in j[\"metadata\"] and \\\n",
" \"expert\" in j[\"metadata\"][\"azdata\"] and \\\n",
" \"expanded_rules\" in j[\"metadata\"][\"azdata\"][\"expert\"]:\n",
"\n",
" rules = j[\"metadata\"][\"azdata\"][\"expert\"][\"expanded_rules\"]\n",
"\n",
" rules.sort() # Sort rules, so they run in priority order (the [0] element). Lowest value first.\n",
"\n",
" # print (f\"EXPERT: There are {len(rules)} rules to evaluate.\")\n",
"\n",
" return rules\n",
"\n",
"def apply_expert_rules(line):\n",
" \"\"\"Determine if the stderr line passed in, matches the regular expressions for any of the 'expert rules', if so\n",
" inject a 'HINT' to the follow-on SOP/TSG to run\"\"\"\n",
"\n",
" global rules\n",
"\n",
" for rule in rules:\n",
" notebook = rule[1]\n",
" cell_type = rule[2]\n",
" output_type = rule[3] # i.e. stream or error\n",
" output_type_name = rule[4] # i.e. ename or name \n",
" output_type_value = rule[5] # i.e. SystemExit or stdout\n",
" details_name = rule[6] # i.e. evalue or text \n",
" expression = rule[7].replace(\"\\\\*\", \"*\") # Something escaped *, and put a \\ in front of it!\n",
"\n",
" if debug_logging:\n",
" print(f\"EXPERT: If rule '{expression}' satisfied', run '{notebook}'.\")\n",
"\n",
" if re.match(expression, line, re.DOTALL):\n",
"\n",
" if debug_logging:\n",
" print(\"EXPERT: MATCH: name = value: '{0}' = '{1}' matched expression '{2}', therefore HINT '{4}'\".format(output_type_name, output_type_value, expression, notebook))\n",
"\n",
" match_found = True\n",
"\n",
" display(Markdown(f'HINT: Use [{notebook}]({notebook}) to resolve this issue.'))\n",
"\n",
"\n",
"\n",
"# Hints for tool retry (on transient fault), known errors and install guide\n",
"print('Common functions defined successfully.')\n",
"\n",
"# Hints for binary (transient fault) retry, (known) error and install guide\n",
"#\n",
"retry_hints = {}\n",
"error_hints = {}\n",
"install_hint = {}\n",
"\n",
"\n",
"print('Common functions defined successfully.')"
"retry_hints = {'kubectl': ['A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond']}\n",
"error_hints = {'kubectl': [['no such host', 'TSG010 - Get configuration contexts', '../monitor-k8s/tsg010-get-kubernetes-contexts.ipynb'], ['No connection could be made because the target machine actively refused it', 'TSG056 - Kubectl fails with No connection could be made because the target machine actively refused it', '../repair/tsg056-kubectl-no-connection-could-be-made.ipynb']]}\n",
"install_hint = {'kubectl': ['SOP036 - Install kubectl command line interface', '../install/sop036-install-kubectl.ipynb']}"
]
},
{
"cell_type": "markdown",
"execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"metadata": {},
"source": [
"### Get Postgres server"
]
@@ -348,11 +400,10 @@
"# Sets the 'server' variable to the spec of the Postgres server\n",
"\n",
"import math\n",
"import json\n",
"\n",
"# If a server was provided, get it\n",
"if namespace and name:\n",
" server = json.loads(run(f'kubectl get postgresqls -n {namespace} {name} -o json', return_output=True))\n",
"if namespace and name and version:\n",
" server = json.loads(run(f'kubectl get postgresql-{version} -n {namespace} {name} -o json', return_output=True))\n",
"else:\n",
" # Otherwise prompt the user to select a server\n",
" servers = json.loads(run(f'kubectl get postgresqls --all-namespaces -o json', return_output=True))['items']\n",
@@ -364,18 +415,19 @@
"\n",
" pad = math.floor(math.log10(len(servers)) + 1) + 3\n",
" for i, s in enumerate(servers):\n",
" print(f'{f\"[{i+1}]\":\u003c{pad}}{full_name(s)}')\n",
" print(f'{f\"[{i+1}]\":<{pad}}{full_name(s)}')\n",
"\n",
" while True:\n",
" try:\n",
" i = int(input('Enter the index of a server'))\n",
" i = int(input('Enter the index of a server to troubleshoot: '))\n",
" except ValueError:\n",
" continue\n",
"\n",
" if i \u003e= 1 and i \u003c= len(servers):\n",
" if i >= 1 and i <= len(servers):\n",
" server = servers[i-1]\n",
" namespace = server['metadata']['namespace']\n",
" name = server['metadata']['name']\n",
" version = server['kind'][len('postgresql-'):]\n",
" break\n",
"\n",
"display(Markdown(f'#### Got server {namespace}.{name}'))"
@@ -383,11 +435,7 @@
},
{
"cell_type": "markdown",
"execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"metadata": {},
"source": [
"### Summarize all resources"
]
@@ -395,15 +443,13 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"tags": []
},
"metadata": {},
"outputs": [],
"source": [
"uid = server['metadata']['uid']\n",
"\n",
"display(Markdown(f'#### Server summary'))\n",
"run(f'kubectl get postgresqls -n {namespace} {name}')\n",
"run(f'kubectl get postgresql-{version} -n {namespace} {name}')\n",
"\n",
"display(Markdown(f'#### Resource summary'))\n",
"run(f'kubectl get sts,pods,pvc,svc,ep -n {namespace} -l postgresqls.arcdata.microsoft.com/cluster-id={uid}')"
@@ -411,11 +457,7 @@
},
{
"cell_type": "markdown",
"execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"metadata": {},
"source": [
"### Troubleshoot the server"
]
@@ -423,22 +465,16 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"tags": []
},
"metadata": {},
"outputs": [],
"source": [
"display(Markdown(f'#### Troubleshooting server {namespace}.{name}'))\n",
"run(f'kubectl describe postgresqls -n {namespace} {name}')"
"run(f'kubectl describe postgresql-{version} -n {namespace} {name}')"
]
},
{
"cell_type": "markdown",
"execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"metadata": {},
"source": [
"### Troubleshoot the pods"
]
@@ -446,9 +482,7 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"tags": []
},
"metadata": {},
"outputs": [],
"source": [
"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",
@@ -471,11 +505,7 @@
},
{
"cell_type": "markdown",
"execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"metadata": {},
"source": [
"### Troubleshoot the containers"
]
@@ -483,9 +513,7 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"tags": []
},
"metadata": {},
"outputs": [],
"source": [
"# Summarize and get logs from each container\n",
@@ -493,7 +521,7 @@
" pod_name = pod['metadata']['name']\n",
" cons = pod['spec']['containers']\n",
" con_statuses = pod['status'].get('containerStatuses', [])\n",
" display(Markdown(f'#### Troubleshooting {len(cons)} container{\"\" if len(cons) \u003c 2 else \"s\"} '\n",
" display(Markdown(f'#### Troubleshooting {len(cons)} container{\"\" if len(cons) < 2 else \"s\"} '\n",
" f'containers for pod {namespace}.{pod_name}'))\n",
"\n",
" for i, con in enumerate(cons):\n",
@@ -509,18 +537,14 @@
" run(f'kubectl logs -n {namespace} {pod_name} {con_name} --tail {tail_lines}')\n",
"\n",
" # Get logs from the previous terminated container if one exists\n",
" if con_restarts \u003e 0:\n",
" if con_restarts > 0:\n",
" display(Markdown(f'#### Logs from previous terminated container {namespace}.{pod_name}/{con_name}'))\n",
" run(f'kubectl logs -n {namespace} {pod_name} {con_name} --tail {tail_lines} --previous')"
]
},
{
"cell_type": "markdown",
"execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"metadata": {},
"source": [
"### Troubleshoot the PersistentVolumeClaims"
]
@@ -528,9 +552,7 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"tags": []
},
"metadata": {},
"outputs": [],
"source": [
"display(Markdown(f'#### Troubleshooting PersistentVolumeClaims'))\n",
@@ -540,12 +562,10 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"tags": []
},
"metadata": {},
"outputs": [],
"source": [
"print(\"Notebook execution is complete.\")"
"print('Notebook execution complete.')"
]
}
],
@@ -556,36 +576,20 @@
"name": "python3",
"display_name": "Python 3"
},
"pansop": {
"related": "",
"azdata": {
"test": {
"strategy": "",
"types": null,
"disable": {
"reason": "",
"workitems": null,
"types": null
"ci": false,
"gci": false
},
"contract": {
"requires": {
"kubectl": {
"installed": true
}
}
},
"target": {
"current": "public",
"final": "public"
},
"internal": {
"parameters": null,
"symlink": false
},
"timeout": "0"
},
"language_info": {
"codemirror_mode": "{ Name: \"\", Version: \"\"}",
"file_extension": "",
"mimetype": "",
"name": "",
"nbconvert_exporter": "",
"pygments_lexer": "",
"version": ""
},
"widgets": []
"side_effects": false
}
}
}

View File

@@ -0,0 +1,5 @@
# Azure Arc Data Services Jupyter Book
## Chapters
1. [Postgres](postgres/readme.md) - notebooks for troubleshooting Postgres on Azure Arc.

View File

@@ -132,7 +132,13 @@
" sys.exit(f'Password is required.')\n",
" confirm_password = getpass.getpass(prompt = 'Confirm password')\n",
" if arc_admin_password != confirm_password:\n",
" sys.exit(f'Passwords do not match.')"
" sys.exit(f'Passwords do not match.')\n",
"\n",
"os.environ[\"SPN_CLIENT_ID\"] = sp_client_id\n",
"os.environ[\"SPN_TENANT_ID\"] = sp_tenant_id\n",
"if \"AZDATA_NB_VAR_SP_CLIENT_SECRET\" in os.environ:\n",
" os.environ[\"SPN_CLIENT_SECRET\"] = os.environ[\"AZDATA_NB_VAR_SP_CLIENT_SECRET\"]\n",
"os.environ[\"SPN_AUTHORITY\"] = \"https://login.microsoftonline.com\""
],
"metadata": {
"azdata_cell_guid": "e7e10828-6cae-45af-8c2f-1484b6d4f9ac",
@@ -182,7 +188,7 @@
"os.environ[\"AZDATA_PASSWORD\"] = arc_admin_password\n",
"if os.name == 'nt':\n",
" print(f'If you don\\'t see output produced by azdata, you can run the following command in a terminal window to check the deployment status:\\n\\t {os.environ[\"AZDATA_NB_VAR_KUBECTL\"]} get pods -n {arc_data_controller_namespace}')\n",
"run_command(f'azdata arc dc create --connectivity-mode 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",
"run_command(f'azdata arc dc create --connectivity-mode {arc_data_controller_connectivity_mode} -n {arc_data_controller_name} -ns {arc_data_controller_namespace} -s {arc_subscription} -g {arc_resource_group} -l {arc_data_controller_location} -sc {arc_data_controller_storage_class} --profile-name {arc_profile}')\n",
"print(f'Azure Arc Data Controller: {arc_data_controller_name} created.') "
],
"metadata": {

View File

@@ -138,7 +138,7 @@
"\n",
"os.environ[\"AZDATA_USERNAME\"] = sql_username\n",
"os.environ[\"AZDATA_PASSWORD\"] = os.environ[\"AZDATA_NB_VAR_SQL_PASSWORD\"]\n",
"cmd = f'azdata arc sql mi create -n {sql_instance_name} -scd {sql_storage_class_data} -scl {sql_storage_class_logs} --replicas {sql_replicas}{cores_request_option}{cores_limit_option}{memory_request_option}{memory_limit_option}'\n",
"cmd = f'azdata arc sql mi create -n {sql_instance_name} -scd {sql_storage_class_data} -scl {sql_storage_class_logs}{cores_request_option}{cores_limit_option}{memory_request_option}{memory_limit_option}'\n",
"out=run_command()"
],
"metadata": {

View File

@@ -2,14 +2,14 @@
"name": "arc",
"displayName": "%arc.displayName%",
"description": "%arc.description%",
"version": "0.9.3",
"version": "0.8.0",
"publisher": "Microsoft",
"preview": true,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
"icon": "images/extension.png",
"engines": {
"vscode": "*",
"azdata": ">=1.28.0"
"azdata": ">=1.26.0"
},
"activationEvents": [
"onCommand:arc.connectToController",
@@ -241,6 +241,99 @@
]
}
]
},
{
"title": "%arc.data.controller.connectivitymode%",
"fields": [
{
"type": "readonly_text",
"label": "%arc.data.controller.connectivitymode.description%",
"labelWidth": "600px"
},
{
"type": "options",
"label": "%arc.data.controller.connectivitymode%",
"required": true,
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_CONNECTIVITY_MODE",
"options": {
"values": [
{
"name": "indirect",
"displayName": "%arc.data.controller.indirect%"
},
{
"name": "direct",
"displayName": "%arc.data.controller.direct%"
}
],
"defaultValue": "%arc.data.controller.indirect%",
"optionsType": "radio"
}
},
{
"type": "readonly_text",
"label": "%arc.data.controller.serviceprincipal.description%",
"labelWidth": "600px",
"links": [
{
"text": "%arc.data.controller.readmore%",
"url": "https://docs.microsoft.com/azure/azure-arc/data/upload-metrics"
}
]
},
{
"label": "%arc.data.controller.spclientid%",
"description": "%arc.data.controller.spclientid.description%",
"variableName": "AZDATA_NB_VAR_SP_CLIENT_ID",
"type": "text",
"required": true,
"defaultValue": "",
"placeHolder": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"enabled": {
"target": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_CONNECTIVITY_MODE",
"value": "direct"
},
"validations": [
{
"type": "regex_match",
"regex": "^[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}$",
"description": "%arc.data.controller.spclientid.validation.description%"
}
]
},
{
"label": "%arc.data.controller.spclientsecret%",
"description": "%arc.data.controller.spclientsecret.description%",
"variableName": "AZDATA_NB_VAR_SP_CLIENT_SECRET",
"type": "password",
"required": true,
"defaultValue": "",
"enabled": {
"target": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_CONNECTIVITY_MODE",
"value": "direct"
}
},
{
"label": "%arc.data.controller.sptenantid%",
"description": "%arc.data.controller.sptenantid.description%",
"variableName": "AZDATA_NB_VAR_SP_TENANT_ID",
"type": "text",
"required": true,
"defaultValue": "",
"enabled": false,
"valueProvider": {
"providerId": "subscription-id-to-tenant-id",
"triggerField": "AZDATA_NB_VAR_ARC_SUBSCRIPTION"
},
"validations": [
{
"type": "regex_match",
"regex": "^[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}$",
"description": "%arc.data.controller.sptenantid.validation.description%"
}
]
}
]
}
]
},
@@ -507,6 +600,12 @@
"type": "readonly_text",
"isEvaluated": true,
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAME)"
},
{
"label": "%arc.data.controller.connectivitymode%",
"type": "readonly_text",
"isEvaluated": true,
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_CONNECTIVITY_MODE)"
}
]
}
@@ -520,7 +619,7 @@
},
{
"name": "azdata",
"version": "20.3.2"
"version": "20.3.1"
}
],
"when": true
@@ -772,7 +871,7 @@
},
{
"name": "azdata",
"version": "20.3.2"
"version": "20.3.1"
}
],
"when": "true"
@@ -900,21 +999,6 @@
{
"title": "%arc.sql.instance.settings.section.title%",
"fields": [
{
"type": "options",
"label": "%arc.sql.replicas.label%",
"description": "%arc.sql.replicas.description%",
"required": true,
"variableName": "AZDATA_NB_VAR_SQL_REPLICAS",
"options": {
"values": [
"1",
"3"
],
"defaultValue": "1",
"optionsType": "radio"
}
},
{
"label": "%arc.storage-class.data.label%",
"description": "%arc.sql.storage-class.data.description%",
@@ -1001,7 +1085,7 @@
},
{
"name": "azdata",
"version": "20.3.2"
"version": "20.3.1"
}
],
"when": "mi-type=arc-mi"
@@ -1031,11 +1115,6 @@
"when": "mi-type=arc-mi"
}
}
],
"resourceDeploymentOptionsSources": [
{
"id": "arc.controllers"
}
]
},
"dependencies": {
@@ -1051,7 +1130,7 @@
"@types/sinon": "^9.0.4",
"@types/uuid": "^8.3.0",
"@types/yamljs": "^0.2.31",
"@microsoft/azdata-test": "^1.5.0",
"@microsoft/azdata-test": "^1.4.0",
"mocha": "^5.2.0",
"mocha-junit-reporter": "^1.17.0",
"mocha-multi-reporters": "^1.1.7",

View File

@@ -22,7 +22,8 @@
"arc.data.controller.cluster.config.profile": "Config profile",
"arc.data.controller.cluster.config.profile.loading": "Loading config profiles",
"arc.data.controller.cluster.config.profile.loadingcompleted": "Loading config profiles complete",
"arc.data.controller.create.azureconfig.title": "Azure Configuration",
"arc.data.controller.create.azureconfig.title": "Azure and Connectivity Configuration",
"arc.data.controller.connectivitymode.description": "Select the connectivity mode for the controller.",
"arc.data.controller.create.controllerconfig.title": "Controller Configuration",
"arc.data.controller.project.details.title": "Azure details",
"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.",
@@ -37,6 +38,18 @@
"arc.data.controller.admin.account.name": "Data controller login",
"arc.data.controller.admin.account.password": "Password",
"arc.data.controller.admin.account.confirm.password": "Confirm password",
"arc.data.controller.connectivitymode": "Connectivity Mode",
"arc.data.controller.direct": "Direct",
"arc.data.controller.indirect": "Indirect",
"arc.data.controller.serviceprincipal.description": "When deploying a controller in direct connected mode a Service Principal is required for connecting to Azure. {0} about how to create this Service Principal and assign it the correct roles.",
"arc.data.controller.spclientid": "Service Principal Client ID",
"arc.data.controller.spclientid.description": "The Client (application) ID of the created Service Principal",
"arc.data.controller.spclientid.validation.description": "The Client ID must be a GUID in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"arc.data.controller.spclientsecret": "Service Principal Client Secret",
"arc.data.controller.spclientsecret.description": "The secret (password) of the Service Principal",
"arc.data.controller.sptenantid": "Service Principal Tenant ID",
"arc.data.controller.sptenantid.description": "The Tenant ID of the Service Principal. This must be the same as the Tenant ID of the subscription selected to create this controller for.",
"arc.data.controller.sptenantid.validation.description": "The tenant ID must be a GUID in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"arc.data.controller.create.summary.title": "Review your configuration",
"arc.data.controller.summary.arc.data.controller": "Azure Arc data controller",
"arc.data.controller.summary.estimated.cost.per.month": "Estimated cost per month",
@@ -84,8 +97,6 @@
"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.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.replicas.label": "Replicas",
"arc.sql.replicas.description": "The number of SQL Managed Instance replicas that will be deployed in your Kubernetes cluster for high availability purposes",
"arc.storage-class.data.label": "Storage Class (Data)",
"arc.sql.storage-class.data.description": "The storage class to be used for data (.mdf)",
"arc.postgres.storage-class.data.description": "The storage class to be used for data persistent volumes",

View File

@@ -10,7 +10,6 @@ import * as loc from '../localizedConstants';
import { throwUnless } from './utils';
export interface KubeClusterContext {
name: string;
namespace?: string;
isCurrentContext: boolean;
}
@@ -19,7 +18,7 @@ export interface KubeClusterContext {
*
* @param configFile
*/
export function getKubeConfigClusterContexts(configFile: string): KubeClusterContext[] {
export function getKubeConfigClusterContexts(configFile: string): Promise<KubeClusterContext[]> {
const config: any = yamljs.load(configFile);
const rawContexts = <any[]>config['contexts'];
throwUnless(rawContexts && rawContexts.length, loc.noContextFound(configFile));
@@ -27,16 +26,16 @@ export function getKubeConfigClusterContexts(configFile: string): KubeClusterCon
throwUnless(currentContext, loc.noCurrentContextFound(configFile));
const contexts: KubeClusterContext[] = [];
rawContexts.forEach(rawContext => {
const name = rawContext.name as string;
const namespace = rawContext.context.namespace as string;
const name = <string>rawContext['name'];
throwUnless(name, loc.noNameInContext(configFile));
contexts.push({
name: name,
namespace: namespace,
isCurrentContext: name === currentContext
});
if (name) {
contexts.push({
name: name,
isCurrentContext: name === currentContext
});
}
});
return contexts;
return Promise.resolve(contexts);
}
/**
@@ -48,23 +47,22 @@ export function getKubeConfigClusterContexts(configFile: string): KubeClusterCon
*
*
* @param clusterContexts
* @param previousClusterContextName
* @param previousClusterContext
* @param throwIfNotFound
*/
export function getCurrentClusterContext(clusterContexts: KubeClusterContext[], previousClusterContextName?: string, throwIfNotFound: boolean = false): KubeClusterContext {
if (previousClusterContextName) {
const previousClusterContext = clusterContexts.find(c => c.name === previousClusterContextName);
if (previousClusterContext) { // if previous cluster context value is found in clusters then return that value
export function getCurrentClusterContext(clusterContexts: KubeClusterContext[], previousClusterContext?: string, throwIfNotFound: boolean = false): string {
if (previousClusterContext) {
if (clusterContexts.find(c => c.name === previousClusterContext)) { // if previous cluster context value is found in clusters then return that value
return previousClusterContext;
} else {
if (throwIfNotFound) {
throw new Error(loc.clusterContextNotFound(previousClusterContextName));
throw new Error(loc.clusterContextNotFound(previousClusterContext));
}
}
}
// if not previousClusterContext or throwIfNotFound was false when previousCLusterContext was not found in the clusterContexts
const currentClusterContext = clusterContexts.find(c => c.isCurrentContext);
const currentClusterContext = clusterContexts.find(c => c.isCurrentContext)?.name;
throwUnless(currentClusterContext !== undefined, loc.noCurrentClusterContext);
return currentClusterContext;
}

View File

@@ -184,30 +184,6 @@ export async function promptAndConfirmPassword(validate: (input: string) => stri
return undefined;
}
export function generateGuid(): string {
let hexValues: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
// c.f. rfc4122 (UUID version 4 = xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx)
let oct: string = '';
let tmp: number;
/* tslint:disable:no-bitwise */
for (let a: number = 0; a < 4; a++) {
tmp = (4294967296 * Math.random()) | 0;
oct += hexValues[tmp & 0xF] +
hexValues[tmp >> 4 & 0xF] +
hexValues[tmp >> 8 & 0xF] +
hexValues[tmp >> 12 & 0xF] +
hexValues[tmp >> 16 & 0xF] +
hexValues[tmp >> 20 & 0xF] +
hexValues[tmp >> 24 & 0xF] +
hexValues[tmp >> 28 & 0xF];
}
// 'Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively'
let clockSequenceHi: string = hexValues[8 + (Math.random() * 4) | 0];
return oct.substr(0, 8) + '-' + oct.substr(9, 4) + '-4' + oct.substr(13, 3) + '-' + clockSequenceHi + oct.substr(16, 3) + '-' + oct.substr(19, 12);
/* tslint:enable:no-bitwise */
}
/**
* Gets the message to display for a given error object that may be a variety of types.
* @param error The error object

View File

@@ -79,7 +79,6 @@ export const resetAllToDefault = localize('arc.resetAllToDefault', "Reset all to
export const resetToDefault = localize('arc.resetToDefault', "Reset to default");
export const troubleshoot = localize('arc.troubleshoot', "Troubleshoot");
export const clickTheNewSupportRequestButton = localize('arc.clickTheNewSupportRequestButton', "Click the new support request button to file a support request in the Azure Portal.");
export const supportRequestNote = localize('arc.supportRequestNote', "Note that the resource configuration must have been uploaded to Azure first in order to open a support request.");
export const running = localize('arc.running', "Running");
export const ready = localize('arc.ready', "Ready");
export const notReady = localize('arc.notReady', "Not Ready");
@@ -97,18 +96,13 @@ export function connectToMSSql(name: string): string { return localize('arc.conn
export function connectToPGSql(name: string): string { return localize('arc.connectToPGSql', "Connect to PostgreSQL Hyperscale - Azure Arc ({0})", name); }
export const passwordToController = localize('arc.passwordToController', "Provide Password to Controller");
export const controllerUrl = localize('arc.controllerUrl', "Controller URL");
export const controllerUrlPlaceholder = localize('arc.controllerUrlPlaceholder', "https://<IP or hostname>:<port>");
export const controllerUrlDescription = localize('arc.controllerUrlDescription', "The Controller URL is necessary if there are multiple clusters with the same namespace - this should generally not be necessary.");
export const serverEndpoint = localize('arc.serverEndpoint', "Server Endpoint");
export const controllerName = localize('arc.controllerName', "Name");
export const controllerNameDescription = localize('arc.controllerNameDescription', "The name to display in the tree view, this is not applied to the controller itself.");
export const controllerKubeConfig = localize('arc.controllerKubeConfig', "Kube Config File Path");
export const controllerClusterContext = localize('arc.controllerClusterContext', "Cluster Context");
export const defaultControllerName = localize('arc.defaultControllerName', "arc-dc");
export const postgresProviderName = localize('arc.postgresProviderName', "PGSQL");
export const miaaProviderName = localize('arc.miaaProviderName', "MSSQL");
export const controllerUsername = localize('arc.controllerUsername', "Controller Username");
export const controllerPassword = localize('arc.controllerPassword', "Controller Password");
export const username = localize('arc.username', "Username");
export const password = localize('arc.password', "Password");
export const rememberPassword = localize('arc.rememberPassword', "Remember Password");
@@ -140,8 +134,6 @@ export const postgresArcProductName = localize('arc.postgresArcProductName', "Az
export const coordinator = localize('arc.coordinator', "Coordinator");
export const worker = localize('arc.worker', "Worker");
export const monitor = localize('arc.monitor', "Monitor");
export const available = localize('arc.available', "Available");
export const issuesDetected = localize('arc.issuesDetected', "Issues Detected");
export const newDatabase = localize('arc.newDatabase', "New Database");
export const databaseName = localize('arc.databaseName', "Database name");
export const enterNewPassword = localize('arc.enterNewPassword', "Enter a new password");
@@ -160,7 +152,6 @@ export const postgresComputeAndStorageDescriptionPartTwo = localize('arc.postgre
export const computeAndStorageDescriptionPartThree = localize('arc.computeAndStorageDescriptionPartThree', "without downtime and by");
export const computeAndStorageDescriptionPartFour = localize('arc.computeAndStorageDescriptionPartFour', "Before doing so, you need to ensure");
export const computeAndStorageDescriptionPartFive = localize('arc.computeAndStorageDescriptionPartFive', "there are sufficient resources available");
export const resourceHealthDescription = localize('arc.resourceHealthDescription', "Resource health can tell you if your resource is running as expected.");
export const computeAndStorageDescriptionPartSix = localize('arc.computeAndStorageDescriptionPartSix', "in your Kubernetes cluster to honor this configuration.");
export const node = localize('arc.node', "node");
export const nodes = localize('arc.nodes', "nodes");
@@ -178,21 +169,14 @@ export const arcResources = localize('arc.arcResources', "Azure Arc Resources");
export const enterANonEmptyPassword = localize('arc.enterANonEmptyPassword', "Enter a non empty password or press escape to exit.");
export const thePasswordsDoNotMatch = localize('arc.thePasswordsDoNotMatch', "The passwords do not match. Confirm the password or press escape to exit.");
export const passwordReset = localize('arc.passwordReset', "Password reset successfully");
export const podOverview = localize('arc.podOverview', "Pod overview");
export const condition = localize('arc.condition', "Condition");
export const details = localize('arc.details', "Details");
export const lastTransition = localize('arc.lastTransition', "Last transition");
export const lastUpdated = localize('arc.lastUpdated', "Last updated");
export const noExternalEndpoint = localize('arc.noExternalEndpoint', "No External Endpoint has been configured so this information isn't available.");
export const podsReady = localize('arc.podsReady', "pods ready");
export const podsPresent = localize('arc.podsPresent', "Pods Present");
export const podsUsedDescription = localize('arc.podsUsedDescription', "Select a pod in the dropdown below for detailed health information.");
export const connectToPostgresDescription = localize('arc.connectToPostgresDescription', "A connection to the server is required to show and set database engine settings, which will require the PostgreSQL Extension to be installed.");
export const postgresExtension = localize('arc.postgresExtension', "microsoft.azuredatastudio-postgresql");
export const podInitialized = localize('arc.podInitialized', "Pod is initialized.");
export const podReady = localize('arc.podReady', "Pod is ready.");
export const noPodIssuesDetected = localize('arc.noPodIssuesDetected', "There arent any known issues affecting this PostgreSQL Hyperscale instance.");
export const podIssuesDetected = localize('arc.podIssuesDetected', "The pods listed below are experiencing issues that may affect performance or availability.");
export const containerReady = localize('arc.containerReady', "Pod containers are ready.");
export const podScheduled = localize('arc.podScheduled', "Pod is schedulable.");
export function rangeSetting(min: string, max: string): string { return localize('arc.rangeSetting', "Value is expected to be in the range {0} - {1}", min, max); }
export function databaseCreated(name: string): string { return localize('arc.databaseCreated', "Database {0} created", name); }
@@ -241,7 +225,6 @@ export function fetchEndpointsFailed(name: string, error: any): string { return
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 fetchEngineSettingsFailed(name: string, error: any): string { return localize('arc.fetchEngineSettingsFailed', "An unexpected error occurred retrieving the engine settings for '{0}'. {1}", name, getErrorMessage(error)); }
export function numberOfIssuesDetected(name: string, issues: number): string { return localize('arc.numberOfIssuesDetected', "• {0} ({1} issues)", name, issues); }
export function instanceDeletionWarning(name: string): string { return localize('arc.instanceDeletionWarning', "Warning! Deleting an instance is permanent and cannot be undone. To delete the instance '{0}' type the name '{0}' below to proceed.", name); }
export function invalidInstanceDeletionName(name: string): string { return localize('arc.invalidInstanceDeletionName', "The value '{0}' does not match the instance name. Try again or press escape to exit", name); }
export function couldNotFindAzureResource(name: string): string { return localize('arc.couldNotFindAzureResource', "Could not find Azure resource for {0}", name); }

View File

@@ -46,20 +46,6 @@ export class ControllerModel {
return this._info;
}
/**
* Gets the controller context to use when executing azdata commands. This is in one of two forms :
*
* If no URL is specified for this controller then just the namespace is used (e.g. test-namespace)
* If a URL is specified then a 3-part name is used, combining the namespace, username and URL separated by
* / (e.g. test-namespace/admin/https://10.91.86.13:30080)
*/
public get controllerContext(): string {
if (this._info.endpoint) {
return `${this._info.namespace}/${this._info.username}/${this._info.endpoint}`;
}
return this._info.namespace;
}
public set info(value: ControllerInfo) {
this._info = value;
this._onInfoUpdated.fire(this._info);
@@ -77,10 +63,10 @@ export class ControllerModel {
* calls from changing the context while commands for this session are being executed.
* @param promptReconnect
*/
public async login(promptReconnect: boolean = false): Promise<void> {
public async acquireAzdataSession(promptReconnect: boolean = false): Promise<azdataExt.AzdataSession> {
let promptForValidClusterContext: boolean = false;
try {
const contexts = getKubeConfigClusterContexts(this.info.kubeConfigFilePath);
const contexts = await getKubeConfigClusterContexts(this.info.kubeConfigFilePath);
getCurrentClusterContext(contexts, this.info.kubeClusterContext, true); // this throws if this.info.kubeClusterContext is not found in 'contexts'
} catch (error) {
const response = await vscode.window.showErrorMessage(loc.clusterContextConfigNoLongerValid(this.info.kubeConfigFilePath, this.info.kubeClusterContext, error), loc.yes, loc.no);
@@ -114,7 +100,8 @@ export class ControllerModel {
}
}
}
await this._azdataApi.azdata.login({ endpoint: this.info.endpoint, namespace: this.info.namespace }, this.info.username, this._password, this.azdataAdditionalEnvVars);
return this._azdataApi.azdata.acquireSession(this.info.url, this.info.username, this._password, this.azdataAdditionalEnvVars);
}
/**
@@ -128,64 +115,67 @@ export class ControllerModel {
await this.refresh(false);
}
}
public async refresh(showErrors: boolean = true): Promise<void> {
// First need to log in to ensure that we're able to authenticate with the controller
await this.login(false);
public async refresh(showErrors: boolean = true, promptReconnect: boolean = false): Promise<void> {
const session = await this.acquireAzdataSession(promptReconnect);
const newRegistrations: Registration[] = [];
await Promise.all([
this._azdataApi.azdata.arc.dc.config.show(this.azdataAdditionalEnvVars, this.controllerContext).then(result => {
this._controllerConfig = result.result;
this.configLastUpdated = new Date();
this._onConfigUpdated.fire(this._controllerConfig);
}).catch(err => {
// If an error occurs show a message so the user knows something failed but still
// fire the event so callers hooking into this can handle the error (e.g. so dashboards don't show the
// loading icon forever)
if (showErrors) {
vscode.window.showErrorMessage(loc.fetchConfigFailed(this.info.name, err));
}
this._onConfigUpdated.fire(this._controllerConfig);
throw err;
}),
this._azdataApi.azdata.arc.dc.endpoint.list(this.azdataAdditionalEnvVars, this.controllerContext).then(result => {
this._endpoints = result.result;
this.endpointsLastUpdated = new Date();
this._onEndpointsUpdated.fire(this._endpoints);
}).catch(err => {
// If an error occurs show a message so the user knows something failed but still
// fire the event so callers can know to update (e.g. so dashboards don't show the
// loading icon forever)
if (showErrors) {
vscode.window.showErrorMessage(loc.fetchEndpointsFailed(this.info.name, err));
}
this._onEndpointsUpdated.fire(this._endpoints);
throw err;
}),
Promise.all([
this._azdataApi.azdata.arc.postgres.server.list(this.azdataAdditionalEnvVars, this.controllerContext).then(result => {
newRegistrations.push(...result.result.map(r => {
return {
instanceName: r.name,
state: r.state,
instanceType: ResourceType.postgresInstances
};
}));
try {
await Promise.all([
this._azdataApi.azdata.arc.dc.config.show(this.azdataAdditionalEnvVars, session).then(result => {
this._controllerConfig = result.result;
this.configLastUpdated = new Date();
this._onConfigUpdated.fire(this._controllerConfig);
}).catch(err => {
// If an error occurs show a message so the user knows something failed but still
// fire the event so callers hooking into this can handle the error (e.g. so dashboards don't show the
// loading icon forever)
if (showErrors) {
vscode.window.showErrorMessage(loc.fetchConfigFailed(this.info.name, err));
}
this._onConfigUpdated.fire(this._controllerConfig);
throw err;
}),
this._azdataApi.azdata.arc.sql.mi.list(this.azdataAdditionalEnvVars, this.controllerContext).then(result => {
newRegistrations.push(...result.result.map(r => {
return {
instanceName: r.name,
state: r.state,
instanceType: ResourceType.sqlManagedInstances
};
}));
this._azdataApi.azdata.arc.dc.endpoint.list(this.azdataAdditionalEnvVars, session).then(result => {
this._endpoints = result.result;
this.endpointsLastUpdated = new Date();
this._onEndpointsUpdated.fire(this._endpoints);
}).catch(err => {
// If an error occurs show a message so the user knows something failed but still
// fire the event so callers can know to update (e.g. so dashboards don't show the
// loading icon forever)
if (showErrors) {
vscode.window.showErrorMessage(loc.fetchEndpointsFailed(this.info.name, err));
}
this._onEndpointsUpdated.fire(this._endpoints);
throw err;
}),
Promise.all([
this._azdataApi.azdata.arc.postgres.server.list(this.azdataAdditionalEnvVars, session).then(result => {
newRegistrations.push(...result.result.map(r => {
return {
instanceName: r.name,
state: r.state,
instanceType: ResourceType.postgresInstances
};
}));
}),
this._azdataApi.azdata.arc.sql.mi.list(this.azdataAdditionalEnvVars, session).then(result => {
newRegistrations.push(...result.result.map(r => {
return {
instanceName: r.name,
state: r.state,
instanceType: ResourceType.sqlManagedInstances
};
}));
})
]).then(() => {
this._registrations = newRegistrations;
this.registrationsLastUpdated = new Date();
this._onRegistrationsUpdated.fire(this._registrations);
})
]).then(() => {
this._registrations = newRegistrations;
this.registrationsLastUpdated = new Date();
this._onRegistrationsUpdated.fire(this._registrations);
})
]);
]);
} finally {
session.dispose();
}
}
public get endpoints(): azdataExt.DcEndpointListResult[] {
@@ -214,6 +204,6 @@ export class ControllerModel {
* property to for use a display label for this controller
*/
public get label(): string {
return `${this.info.name} (${this.controllerContext})`;
return `${this.info.name} (${this.info.url})`;
}
}

View File

@@ -71,9 +71,11 @@ export class MiaaModel extends ResourceModel {
return this._refreshPromise.promise;
}
this._refreshPromise = new Deferred();
let session: azdataExt.AzdataSession | undefined = undefined;
try {
session = await this.controllerModel.acquireAzdataSession();
try {
const result = await this._azdataApi.azdata.arc.sql.mi.show(this.info.name, this.controllerModel.azdataAdditionalEnvVars, this.controllerModel.controllerContext);
const result = await this._azdataApi.azdata.arc.sql.mi.show(this.info.name, this.controllerModel.azdataAdditionalEnvVars, session);
this._config = result.result;
this.configLastUpdated = new Date();
this._onConfigUpdated.fire(this._config);
@@ -107,6 +109,7 @@ export class MiaaModel extends ResourceModel {
this._refreshPromise.reject(err);
throw err;
} finally {
session?.dispose();
this._refreshPromise = undefined;
}
}

View File

@@ -53,7 +53,10 @@ export class PostgresModel extends ResourceModel {
/** Returns the major version of Postgres */
public get engineVersion(): string | undefined {
return this._config?.spec.engine.version;
const kind = this._config?.kind;
return kind
? kind.substring(kind.lastIndexOf('-') + 1)
: undefined;
}
/** Returns the IP address and port of Postgres */
@@ -118,8 +121,10 @@ export class PostgresModel extends ResourceModel {
return this._refreshPromise.promise;
}
this._refreshPromise = new Deferred();
let session: azdataExt.AzdataSession | undefined = undefined;
try {
this._config = (await this._azdataApi.azdata.arc.postgres.server.show(this.info.name, this.controllerModel.azdataAdditionalEnvVars, this.controllerModel.controllerContext)).result;
session = await this.controllerModel.acquireAzdataSession();
this._config = (await this._azdataApi.azdata.arc.postgres.server.show(this.info.name, this.controllerModel.azdataAdditionalEnvVars, session)).result;
this.configLastUpdated = new Date();
this._onConfigUpdated.fire(this._config);
this._refreshPromise.resolve();
@@ -127,6 +132,7 @@ export class PostgresModel extends ResourceModel {
this._refreshPromise.reject(err);
throw err;
} finally {
session?.dispose();
this._refreshPromise = undefined;
}
}

View File

@@ -30,7 +30,7 @@ export class ArcControllersOptionsSourceProvider implements rd.IOptionsSourcePro
const controller = (await getRegisteredDataControllers(this._treeProvider)).find(ci => ci.label === controllerLabel);
throwUnless(controller !== undefined, loc.noControllerInfoFound(controllerLabel));
switch (variableName) {
case 'endpoint': return controller.info.endpoint || '';
case 'endpoint': return controller.info.url;
case 'username': return controller.info.username;
case 'kubeConfig': return controller.info.kubeConfigFilePath;
case 'clusterContext': return controller.info.kubeClusterContext;

View File

@@ -51,7 +51,7 @@ describe('KubeUtils', function (): void {
contexts[1].name.should.equal('kubernetes-admin@kubernetes', `test: ${testName} failed`);
contexts[1].isCurrentContext.should.be.false(`test: ${testName} failed`);
};
verifyContexts(getKubeConfigClusterContexts(configFile), 'getKubeConfigClusterContexts');
verifyContexts(await getKubeConfigClusterContexts(configFile), 'getKubeConfigClusterContexts');
});
it('throws error when unable to load config file', async () => {
const error = new Error('unknown error accessing file');

View File

@@ -10,81 +10,77 @@ import * as azdataExt from 'azdata-ext';
*/
export class FakeAzdataApi implements azdataExt.IAzdataApi {
private _arcApi = {
dc: {
create(_namespace: string, _name: string, _connectivityMode: string, _resourceGroup: string, _location: string, _subscription: string, _profileName?: string, _storageClass?: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
endpoint: {
async list(): Promise<azdataExt.AzdataOutput<azdataExt.DcEndpointListResult[]>> { return <any>{ result: [] }; }
},
config: {
list(): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigListResult[]>> { throw new Error('Method not implemented.'); },
async show(): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigShowResult>> { return <any>{ result: undefined! }; }
}
},
postgres: {
server: {
postgresInstances: <azdataExt.PostgresServerListResult[]>[],
delete(_name: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
async list(): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerListResult[]>> { return { result: this.postgresInstances, logs: [], stdout: [], stderr: [] }; },
show(_name: string): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerShowResult>> { throw new Error('Method not implemented.'); },
edit(
_name: string,
_args: {
adminPassword?: boolean,
coresLimit?: string,
coresRequest?: string,
engineSettings?: string,
extensions?: string,
memoryLimit?: string,
memoryRequest?: string,
noWait?: boolean,
port?: number,
replaceEngineSettings?: boolean,
workers?: number
},
_additionalEnvVars?: azdataExt.AdditionalEnvVars
): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); }
}
},
sql: {
mi: {
miaaInstances: <azdataExt.SqlMiListResult[]>[],
delete(_name: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
async list(): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiListResult[]>> { return { logs: [], stdout: [], stderr: [], result: this.miaaInstances }; },
show(_name: string): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiShowResult>> { throw new Error('Method not implemented.'); },
edit(
_name: string,
_args: {
coresLimit?: string,
coresRequest?: string,
memoryLimit?: string,
memoryRequest?: string,
noWait?: boolean
}): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); }
}
}
};
public set postgresInstances(instances: azdataExt.PostgresServerListResult[]) {
this._arcApi.postgres.server.postgresInstances = instances;
}
public set miaaInstances(instances: azdataExt.SqlMiListResult[]) {
this._arcApi.sql.mi.miaaInstances = instances;
}
public postgresInstances: azdataExt.PostgresServerListResult[] = [];
public miaaInstances: azdataExt.SqlMiListResult[] = [];
//
// API Implementation
//
public get arc() {
return this._arcApi;
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
},
_engineVersion?: string,
_additionalEnvVars?: azdataExt.AdditionalEnvVars
): 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.'); },
edit(
_name: string,
_args: {
coresLimit?: string,
coresRequest?: string,
memoryLimit?: string,
memoryRequest?: string,
noWait?: boolean
}): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); }
}
}
};
}
getPath(): Promise<string> {
throw new Error('Method not implemented.');
}
login(_endpointOrNamespace: azdataExt.EndpointOrNamespace, _username: string, _password: string, _additionalEnvVars: azdataExt.AdditionalEnvVars = {}, _azdataContext?: string): Promise<azdataExt.AzdataOutput<void>> {
login(_endpoint: string, _username: string, _password: string): Promise<azdataExt.AzdataOutput<void>> {
return <any>undefined;
}
acquireSession(_endpoint: string, _username: string, _password: string): Promise<azdataExt.AzdataSession> {
return Promise.resolve({ dispose: () => { } });
}
version(): Promise<azdataExt.AzdataOutput<string>> {
throw new Error('Method not implemented.');
}

View File

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

View File

@@ -22,20 +22,6 @@ interface ExtensionGlobalMemento extends vscode.Memento {
setKeysForSync(keys: string[]): void;
}
function getDefaultControllerInfo(): ControllerInfo {
return {
id: uuid(),
endpoint: '127.0.0.1',
kubeConfigFilePath: '/path/to/.kube/config',
kubeClusterContext: 'currentCluster',
username: 'admin',
name: 'arc',
namespace: 'arc-ns',
rememberPassword: true,
resources: []
};
}
describe('ControllerModel', function (): void {
afterEach(function (): void {
sinon.restore();
@@ -53,15 +39,15 @@ describe('ControllerModel', function (): void {
beforeEach(function (): void {
sinon.stub(ConnectToControllerDialog.prototype, 'showDialog');
sinon.stub(kubeUtils, 'getKubeConfigClusterContexts').returns([{ name: 'currentCluster', isCurrentContext: true }]);
sinon.stub(kubeUtils, 'getKubeConfigClusterContexts').resolves([{ name: 'currentCluster', isCurrentContext: true }]);
sinon.stub(vscode.window, 'showErrorMessage').resolves(<any>loc.yes);
});
it('Rejected with expected error when user cancels', async function (): Promise<void> {
// Returning an undefined model here indicates that the dialog closed without clicking "Ok" - usually through the user clicking "Cancel"
sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve(undefined));
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo());
await should(model.login()).be.rejectedWith(new UserCancelledError(loc.userCancelledError));
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
await should(model.acquireAzdataSession()).be.rejectedWith(new UserCancelledError(loc.userCancelledError));
});
it('Reads password from cred store', async function (): Promise<void> {
@@ -76,13 +62,13 @@ describe('ControllerModel', function (): void {
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataMock.setup(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo());
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
await model.login();
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
await model.acquireAzdataSession();
azdataMock.verify(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
});
it('Prompt for password when not in cred store', async function (): Promise<void> {
@@ -97,18 +83,18 @@ describe('ControllerModel', function (): void {
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataMock.setup(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
// Set up dialog to return new model with our password
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo(), password);
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password);
sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password }));
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo());
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
await model.login();
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
await model.acquireAzdataSession();
azdataMock.verify(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
});
it('Prompt for password when rememberPassword is true but prompt reconnect is true', async function (): Promise<void> {
@@ -122,19 +108,19 @@ describe('ControllerModel', function (): void {
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataMock.setup(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
// Set up dialog to return new model with our new password from the reprompt
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo(), password);
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password);
const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password }));
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo());
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
await model.login(true);
await model.acquireAzdataSession(true);
should(waitForCloseStub.called).be.true('waitForClose should have been called');
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
azdataMock.verify(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
});
it('Prompt for password when we already have a password but prompt reconnect is true', async function (): Promise<void> {
@@ -148,20 +134,20 @@ describe('ControllerModel', function (): void {
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataMock.setup(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
// Set up dialog to return new model with our new password from the reprompt
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo(), password);
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password);
const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password }));
// Set up original model with a password
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo(), 'originalPassword');
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, 'originalPassword');
await model.login(true);
await model.acquireAzdataSession(true);
should(waitForCloseStub.called).be.true('waitForClose should have been called');
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
azdataMock.verify(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
});
it('Model values are updated correctly when modified during reconnect', async function (): Promise<void> {
@@ -176,7 +162,7 @@ describe('ControllerModel', function (): void {
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataMock.setup(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
@@ -184,19 +170,27 @@ describe('ControllerModel', function (): void {
const originalPassword = 'originalPassword';
const model = new ControllerModel(
treeDataProvider,
getDefaultControllerInfo(),
{
id: uuid(),
url: '127.0.0.1',
kubeConfigFilePath: '/path/to/.kube/config',
kubeClusterContext: 'currentCluster',
username: 'admin',
name: 'arc',
rememberPassword: false,
resources: []
},
originalPassword
);
await treeDataProvider.addOrUpdateController(model, originalPassword);
const newInfo: ControllerInfo = {
id: model.info.id, // The ID stays the same since we're just re-entering information for the same model
endpoint: 'newUrl',
url: 'newUrl',
kubeConfigFilePath: '/path/to/.kube/config',
kubeClusterContext: 'currentCluster',
username: 'newUser',
name: 'newName',
namespace: 'newNamespace',
rememberPassword: true,
resources: []
};
@@ -209,7 +203,7 @@ describe('ControllerModel', function (): void {
const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve(
{ controllerModel: newModel, password: newPassword }));
await model.login(true);
await model.acquireAzdataSession(true);
should(waitForCloseStub.called).be.true('waitForClose should have been called');
should((await treeDataProvider.getChildren()).length).equal(1, 'Tree Data provider should still only have 1 node');
should(model.info).deepEqual(newInfo, 'Model info should have been updated');

View File

@@ -1,544 +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 { PGResourceInfo, ResourceType } from 'arc';
import * as azdataExt from 'azdata-ext';
import * as azdata from 'azdata';
import * as should from 'should';
import * as sinon from 'sinon';
import * as TypeMoq from 'typemoq';
import * as vscode from 'vscode';
import { generateGuid } from '../../common/utils';
import { UserCancelledError } from '../../common/api';
import { ControllerModel, Registration } from '../../models/controllerModel';
import { PostgresModel, EngineSettingsModel } from '../../models/postgresModel';
import { ConnectToPGSqlDialog } from '../../ui/dialogs/connectPGDialog';
import { AzureArcTreeDataProvider } from '../../ui/tree/azureArcTreeDataProvider';
import { FakeControllerModel } from '../mocks/fakeControllerModel';
import { FakeAzdataApi } from '../mocks/fakeAzdataApi';
export const FakePostgresServerShowOutput: azdataExt.AzdataOutput<azdataExt.PostgresServerShowResult> = {
logs: [],
stdout: [],
stderr: [],
result: {
apiVersion: 'version',
kind: 'postgresql',
metadata: {
creationTimestamp: '',
generation: 1,
name: 'pgt',
namespace: 'ns',
resourceVersion: '',
selfLink: '',
uid: '',
},
spec: {
engine: {
extensions: [{ name: '' }],
settings: {
default: { ['']: '' }
},
version: ''
},
scale: {
shards: 0,
workers: 0
},
scheduling: {
default: {
resources: {
requests: {
cpu: '',
memory: ''
},
limits: {
cpu: '',
memory: ''
}
}
}
},
service: {
type: '',
port: 0
},
storage: {
data: {
className: '',
size: ''
},
logs: {
className: '',
size: ''
},
backups: {
className: '',
size: ''
}
}
},
status: {
externalEndpoint: '127.0.0.1:5432',
readyPods: '',
state: '',
logSearchDashboard: '',
metricsDashboard: '',
podsStatus: [{
conditions: [{
lastTransitionTime: '',
message: '',
reason: '',
status: '',
type: '',
}],
name: '',
role: '',
}]
}
}
};
describe('PostgresModel', function (): void {
let controllerModel: ControllerModel;
let postgresModel: PostgresModel;
let azdataApi: azdataExt.IAzdataApi;
afterEach(function (): void {
sinon.restore();
});
beforeEach(async () => {
// Setup Controller Model
controllerModel = new FakeControllerModel();
//Stub calling azdata login and acquiring session
sinon.stub(controllerModel, 'login').returns(Promise.resolve());
// Stub the azdata CLI API
azdataApi = new FakeAzdataApi();
const azdataExt = TypeMoq.Mock.ofType<azdataExt.IExtension>();
azdataExt.setup(x => x.azdata).returns(() => azdataApi);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExt.object });
});
describe('refresh', function (): void {
beforeEach(async () => {
// Setup PostgresModel
const postgresResource: PGResourceInfo = { name: 'pgt', resourceType: '' };
const registration: Registration = { instanceName: '', state: '', instanceType: ResourceType.postgresInstances };
postgresModel = new PostgresModel(controllerModel, postgresResource, registration, new AzureArcTreeDataProvider(TypeMoq.Mock.ofType<vscode.ExtensionContext>().object));
});
it('Updates model to expected config', async function (): Promise<void> {
const postgresShowStub = sinon.stub(azdataApi.arc.postgres.server, 'show').resolves(FakePostgresServerShowOutput);
await postgresModel.refresh();
sinon.assert.calledOnceWithExactly(postgresShowStub, postgresModel.info.name, sinon.match.any, sinon.match.any);
sinon.assert.match(postgresModel.config, FakePostgresServerShowOutput.result);
});
it('Updates onConfigLastUpdated when model is refreshed', async function (): Promise<void> {
const postgresShowStub = sinon.stub(azdataApi.arc.postgres.server, 'show').resolves(FakePostgresServerShowOutput);
await postgresModel.refresh();
sinon.assert.calledOnceWithExactly(postgresShowStub, postgresModel.info.name, sinon.match.any, sinon.match.any);
should(postgresModel.configLastUpdated).be.Date();
});
it('Calls onConfigUpdated event when model is refreshed', async function (): Promise<void> {
const postgresShowStub = sinon.stub(azdataApi.arc.postgres.server, 'show').resolves(FakePostgresServerShowOutput);
const configUpdatedEvent = sinon.spy(vscode.EventEmitter.prototype, 'fire');
await postgresModel.refresh();
sinon.assert.calledOnceWithExactly(postgresShowStub, postgresModel.info.name, sinon.match.any, sinon.match.any);
sinon.assert.calledOnceWithExactly(configUpdatedEvent, postgresModel.config);
});
it('Expected exception is thrown', async function (): Promise<void> {
// Stub 'azdata arc postgres server show' to throw an exception
const error = new Error('something bad happened');
sinon.stub(azdataApi.arc.postgres.server, 'show').throws(error);
await should(postgresModel.refresh()).be.rejectedWith(error);
});
});
describe('getConnectionProfile', function (): void {
beforeEach(async () => {
// Setup PostgresModel
const postgresResource: PGResourceInfo = { name: 'pgt', resourceType: '', userName: 'postgres', connectionId: '12345678' };
const registration: Registration = { instanceName: '', state: '', instanceType: ResourceType.postgresInstances };
postgresModel = new PostgresModel(controllerModel, postgresResource, registration, new AzureArcTreeDataProvider(TypeMoq.Mock.ofType<vscode.ExtensionContext>().object));
sinon.stub(azdataApi.arc.postgres.server, 'show').resolves(FakePostgresServerShowOutput);
//Call to provide external endpoint
await postgresModel.refresh();
});
it('Rejected with expected error when user cancels', async function (): Promise<void> {
const close = sinon.stub(ConnectToPGSqlDialog.prototype, 'waitForClose').returns(Promise.resolve(undefined));
await should(postgresModel['getConnectionProfile']()).be.rejectedWith(new UserCancelledError());
sinon.assert.calledOnce(close);
});
it('Show dialog prompt if password not found', async function (): Promise<void> {
const connect = sinon.stub(azdata.connection, 'connect');
const cancelButtonMock = TypeMoq.Mock.ofType<azdata.window.Button>();
cancelButtonMock.setup((x: any) => x.then).returns(() => undefined);
const dialogMock = TypeMoq.Mock.ofType<azdata.window.Dialog>();
dialogMock.setup(x => x.cancelButton).returns(() => cancelButtonMock.object);
dialogMock.setup((x: any) => x.then).returns(() => undefined);
const show = sinon.stub(azdata.window, 'createModelViewDialog').returns(dialogMock.object);
sinon.stub(azdata.window, 'openDialog');
const iconnectionProfileMock = TypeMoq.Mock.ofType<azdata.IConnectionProfile>();
iconnectionProfileMock.setup((x: any) => x.then).returns(() => undefined);
const close = sinon.stub(ConnectToPGSqlDialog.prototype, 'waitForClose').returns(Promise.resolve(iconnectionProfileMock.object));
await postgresModel['getConnectionProfile']();
sinon.assert.notCalled(connect);
sinon.assert.calledOnce(show);
sinon.assert.calledOnce(close);
});
it('Reads password from cred store and no dialog prompt', async function (): Promise<void> {
const password = generateGuid();
// Set up cred store to return our password
const credProviderMock = TypeMoq.Mock.ofType<azdata.CredentialProvider>();
credProviderMock.setup(x => x.readCredential(TypeMoq.It.isAny())).returns(() => Promise.resolve({ credentialId: 'id', password: password }));
credProviderMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.credentials, 'getProvider').returns(Promise.resolve(credProviderMock.object));
const connectionResultMock = TypeMoq.Mock.ofType<azdata.ConnectionResult>();
connectionResultMock.setup(x => x.connected).returns(() => true);
connectionResultMock.setup((x: any) => x.then).returns(() => undefined);
const connect = sinon.stub(azdata.connection, 'connect').returns(Promise.resolve(connectionResultMock.object));
const cancelButtonMock = TypeMoq.Mock.ofType<azdata.window.Button>();
cancelButtonMock.setup((x: any) => x.then).returns(() => undefined);
const dialogMock = TypeMoq.Mock.ofType<azdata.window.Dialog>();
dialogMock.setup(x => x.cancelButton).returns(() => cancelButtonMock.object);
dialogMock.setup((x: any) => x.then).returns(() => undefined);
const show = sinon.stub(azdata.window, 'createModelViewDialog').returns(dialogMock.object);
sinon.stub(azdata.window, 'openDialog');
const treeSave = sinon.spy(AzureArcTreeDataProvider.prototype, 'saveControllers');
await postgresModel['getConnectionProfile']();
sinon.assert.calledOnce(connect);
sinon.assert.notCalled(show);
sinon.assert.calledOnce(treeSave);
});
it('Reads password from cred store and connect fails, show dialog prompt', async function (): Promise<void> {
const password = generateGuid();
// Set up cred store to return our password
const credProviderMock = TypeMoq.Mock.ofType<azdata.CredentialProvider>();
credProviderMock.setup(x => x.readCredential(TypeMoq.It.isAny())).returns(() => Promise.resolve({ credentialId: 'id', password: password }));
credProviderMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.credentials, 'getProvider').returns(Promise.resolve(credProviderMock.object));
const connectionResultMock = TypeMoq.Mock.ofType<azdata.ConnectionResult>();
connectionResultMock.setup(x => x.connected).returns(() => false);
connectionResultMock.setup((x: any) => x.then).returns(() => undefined);
const connect = sinon.stub(azdata.connection, 'connect').returns(Promise.resolve(connectionResultMock.object));
const iconnectionProfileMock = TypeMoq.Mock.ofType<azdata.IConnectionProfile>();
iconnectionProfileMock.setup((x: any) => x.then).returns(() => undefined);
const close = sinon.stub(ConnectToPGSqlDialog.prototype, 'waitForClose').returns(Promise.resolve(iconnectionProfileMock.object));
const cancelButtonMock = TypeMoq.Mock.ofType<azdata.window.Button>();
cancelButtonMock.setup((x: any) => x.then).returns(() => undefined);
const dialogMock = TypeMoq.Mock.ofType<azdata.window.Dialog>();
dialogMock.setup(x => x.cancelButton).returns(() => cancelButtonMock.object);
dialogMock.setup((x: any) => x.then).returns(() => undefined);
const show = sinon.stub(azdata.window, 'createModelViewDialog').returns(dialogMock.object);
sinon.stub(azdata.window, 'openDialog');
await postgresModel['getConnectionProfile']();
sinon.assert.calledOnce(connect);
sinon.assert.calledOnce(show);
sinon.assert.calledOnce(close);
});
it('Show dialog prompt if username not found', async function (): Promise<void> {
// Setup PostgresModel without username
const postgresResource: PGResourceInfo = { name: 'pgt', resourceType: '', connectionId: '12345678' };
const registration: Registration = { instanceName: '', state: '', instanceType: ResourceType.postgresInstances };
let postgresModelNew = new PostgresModel(controllerModel, postgresResource, registration, new AzureArcTreeDataProvider(TypeMoq.Mock.ofType<vscode.ExtensionContext>().object));
await postgresModelNew.refresh();
const password = generateGuid();
// Set up cred store to return our password
const credProviderMock = TypeMoq.Mock.ofType<azdata.CredentialProvider>();
credProviderMock.setup(x => x.readCredential(TypeMoq.It.isAny())).returns(() => Promise.resolve({ credentialId: 'id', password: password }));
credProviderMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.credentials, 'getProvider').returns(Promise.resolve(credProviderMock.object));
const connect = sinon.stub(azdata.connection, 'connect');
const cancelButtonMock = TypeMoq.Mock.ofType<azdata.window.Button>();
cancelButtonMock.setup((x: any) => x.then).returns(() => undefined);
const dialogMock = TypeMoq.Mock.ofType<azdata.window.Dialog>();
dialogMock.setup(x => x.cancelButton).returns(() => cancelButtonMock.object);
dialogMock.setup((x: any) => x.then).returns(() => undefined);
const show = sinon.stub(azdata.window, 'createModelViewDialog').returns(dialogMock.object);
sinon.stub(azdata.window, 'openDialog');
const iconnectionProfileMock = TypeMoq.Mock.ofType<azdata.IConnectionProfile>();
iconnectionProfileMock.setup((x: any) => x.then).returns(() => undefined);
const close = sinon.stub(ConnectToPGSqlDialog.prototype, 'waitForClose').returns(Promise.resolve(iconnectionProfileMock.object));
await postgresModelNew['getConnectionProfile']();
sinon.assert.notCalled(connect);
sinon.assert.calledOnce(show);
sinon.assert.calledOnce(close);
});
it('Shows dialog prompt if no connection id', async function (): Promise<void> {
// Setup PostgresModel without connectionId
const postgresResource: PGResourceInfo = { name: 'pgt', resourceType: '' };
const registration: Registration = { instanceName: '', state: '', instanceType: ResourceType.postgresInstances };
let postgresModelNew = new PostgresModel(controllerModel, postgresResource, registration, new AzureArcTreeDataProvider(TypeMoq.Mock.ofType<vscode.ExtensionContext>().object));
await postgresModelNew.refresh();
const provider = sinon.stub(azdata.credentials, 'getProvider');
const connect = sinon.stub(azdata.connection, 'connect');
const cancelButtonMock = TypeMoq.Mock.ofType<azdata.window.Button>();
cancelButtonMock.setup((x: any) => x.then).returns(() => undefined);
const dialogMock = TypeMoq.Mock.ofType<azdata.window.Dialog>();
dialogMock.setup(x => x.cancelButton).returns(() => cancelButtonMock.object);
dialogMock.setup((x: any) => x.then).returns(() => undefined);
const show = sinon.stub(azdata.window, 'createModelViewDialog').returns(dialogMock.object);
sinon.stub(azdata.window, 'openDialog');
const iconnectionProfileMock = TypeMoq.Mock.ofType<azdata.IConnectionProfile>();
iconnectionProfileMock.setup((x: any) => x.then).returns(() => undefined);
const close = sinon.stub(ConnectToPGSqlDialog.prototype, 'waitForClose').returns(Promise.resolve(iconnectionProfileMock.object));
await postgresModelNew['getConnectionProfile']();
sinon.assert.notCalled(provider);
sinon.assert.notCalled(connect);
sinon.assert.calledOnce(show);
sinon.assert.calledOnce(close);
});
});
describe('getEngineSettings', function (): void {
beforeEach(async () => {
// Setup PostgresModel
const postgresResource: PGResourceInfo = { name: 'pgt', resourceType: '', userName: 'postgres', connectionId: '12345678' };
const registration: Registration = { instanceName: '', state: '', instanceType: ResourceType.postgresInstances };
postgresModel = new PostgresModel(controllerModel, postgresResource, registration, new AzureArcTreeDataProvider(TypeMoq.Mock.ofType<vscode.ExtensionContext>().object));
//Stub calling refresh postgres model
sinon.stub(azdataApi.arc.postgres.server, 'show').resolves(FakePostgresServerShowOutput);
//Stub how to get connection profile
const iconnectionProfileMock = TypeMoq.Mock.ofType<azdata.IConnectionProfile>();
iconnectionProfileMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(ConnectToPGSqlDialog.prototype, 'waitForClose').returns(Promise.resolve(iconnectionProfileMock.object));
const cancelButtonMock = TypeMoq.Mock.ofType<azdata.window.Button>();
cancelButtonMock.setup((x: any) => x.then).returns(() => undefined);
const dialogMock = TypeMoq.Mock.ofType<azdata.window.Dialog>();
dialogMock.setup(x => x.cancelButton).returns(() => cancelButtonMock.object);
dialogMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.window, 'createModelViewDialog').returns(dialogMock.object);
sinon.stub(azdata.window, 'openDialog');
sinon.stub(azdata.connection, 'getUriForConnection');
//Call to provide external endpoint
await postgresModel.refresh();
});
it('Throw error when trying to connect fails', async function (): Promise<void> {
const errorMessage = 'Mock connection fail occured';
const connectionResultMock = TypeMoq.Mock.ofType<azdata.ConnectionResult>();
connectionResultMock.setup(x => x.connected).returns(() => false);
connectionResultMock.setup(x => x.errorMessage).returns(() => errorMessage);
connectionResultMock.setup((x: any) => x.then).returns(() => undefined);
const connect = sinon.stub(azdata.connection, 'connect').returns(Promise.resolve(connectionResultMock.object));
await should(postgresModel.getEngineSettings()).be.rejectedWith(new Error(errorMessage));
sinon.assert.calledOnce(connect);
});
it('Update active connection id when connect passes', async function (): Promise<void> {
const connectionID = '098765';
const connectionResultMock = TypeMoq.Mock.ofType<azdata.ConnectionResult>();
connectionResultMock.setup(x => x.connected).returns(() => true);
connectionResultMock.setup(x => x.connectionId).returns(() => connectionID);
connectionResultMock.setup((x: any) => x.then).returns(() => undefined);
const connect = sinon.stub(azdata.connection, 'connect').returns(Promise.resolve(connectionResultMock.object));
const array: azdata.DbCellValue[][] = [];
const executeMock = TypeMoq.Mock.ofType<azdata.SimpleExecuteResult>();
executeMock.setup(x => x.rows).returns(() => array);
executeMock.setup((x: any) => x.then).returns(() => undefined);
const providerMock = TypeMoq.Mock.ofType<azdata.QueryProvider>();
providerMock.setup(x => x.runQueryAndReturn(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(async () => executeMock.object);
providerMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.dataprotocol, 'getProvider').returns(providerMock.object);
await postgresModel.getEngineSettings();
sinon.assert.calledOnce(connect);
sinon.assert.match(postgresModel['_activeConnectionId'], connectionID);
});
it('Updates engineSettingsLastUpdated after populating engine settings', async function (): Promise<void> {
const connectionResultMock = TypeMoq.Mock.ofType<azdata.ConnectionResult>();
connectionResultMock.setup(x => x.connected).returns(() => true);
connectionResultMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.connection, 'connect').returns(Promise.resolve(connectionResultMock.object));
const array: azdata.DbCellValue[][] = [];
const executeMock = TypeMoq.Mock.ofType<azdata.SimpleExecuteResult>();
executeMock.setup(x => x.rows).returns(() => array);
executeMock.setup((x: any) => x.then).returns(() => undefined);
const providerMock = TypeMoq.Mock.ofType<azdata.QueryProvider>();
providerMock.setup(x => x.runQueryAndReturn(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(async () => executeMock.object);
providerMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.dataprotocol, 'getProvider').returns(providerMock.object);
await postgresModel.getEngineSettings();
should(postgresModel.engineSettingsLastUpdated).be.Date();
});
it('Calls onEngineSettingsUpdated event after populating engine settings', async function (): Promise<void> {
const connectionResultMock = TypeMoq.Mock.ofType<azdata.ConnectionResult>();
connectionResultMock.setup(x => x.connected).returns(() => true);
connectionResultMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.connection, 'connect').returns(Promise.resolve(connectionResultMock.object));
const array: azdata.DbCellValue[][] = [];
const executeMock = TypeMoq.Mock.ofType<azdata.SimpleExecuteResult>();
executeMock.setup(x => x.rows).returns(() => array);
executeMock.setup((x: any) => x.then).returns(() => undefined);
const providerMock = TypeMoq.Mock.ofType<azdata.QueryProvider>();
providerMock.setup(x => x.runQueryAndReturn(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(async () => executeMock.object);
providerMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.dataprotocol, 'getProvider').returns(providerMock.object);
const onEngineSettingsUpdated = sinon.spy(vscode.EventEmitter.prototype, 'fire');
await postgresModel.getEngineSettings();
sinon.assert.calledOnceWithExactly(onEngineSettingsUpdated, postgresModel.workerNodesEngineSettings);
});
it('Populating engine settings skips certain parameters', async function (): Promise<void> {
const connectionResultMock = TypeMoq.Mock.ofType<azdata.ConnectionResult>();
connectionResultMock.setup(x => x.connected).returns(() => true);
connectionResultMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.connection, 'connect').returns(Promise.resolve(connectionResultMock.object));
const rows: azdata.DbCellValue[][] = [
[{
displayValue: 'archive_timeout',
isNull: false,
invariantCultureDisplayValue: ''
}]
];
const executeMock = TypeMoq.Mock.ofType<azdata.SimpleExecuteResult>();
executeMock.setup(x => x.rows).returns(() => rows);
executeMock.setup((x: any) => x.then).returns(() => undefined);
const providerMock = TypeMoq.Mock.ofType<azdata.QueryProvider>();
providerMock.setup(x => x.runQueryAndReturn(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(async () => executeMock.object);
providerMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.dataprotocol, 'getProvider').returns(providerMock.object);
await postgresModel.getEngineSettings();
should(postgresModel.workerNodesEngineSettings.pop()).be.undefined();
});
it('Populates engine settings accurately', async function (): Promise<void> {
const connectionResultMock = TypeMoq.Mock.ofType<azdata.ConnectionResult>();
connectionResultMock.setup(x => x.connected).returns(() => true);
connectionResultMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.connection, 'connect').returns(Promise.resolve(connectionResultMock.object));
const rows: azdata.DbCellValue[][] = [
[{
displayValue: 'test0',
isNull: false,
invariantCultureDisplayValue: ''
},
{
displayValue: 'test1',
isNull: false,
invariantCultureDisplayValue: ''
},
{
displayValue: 'test2',
isNull: false,
invariantCultureDisplayValue: ''
},
{
displayValue: 'test3',
isNull: false,
invariantCultureDisplayValue: ''
},
{
displayValue: 'test4',
isNull: false,
invariantCultureDisplayValue: ''
},
{
displayValue: 'test5',
isNull: false,
invariantCultureDisplayValue: ''
},
{
displayValue: 'test6',
isNull: false,
invariantCultureDisplayValue: ''
}],
];
const engineSettingsModelCompare: EngineSettingsModel = {
parameterName: 'test0',
value: 'test1',
description: 'test2',
min: 'test3',
max: 'test4',
options: 'test5',
type: 'test6'
};
const executeMock = TypeMoq.Mock.ofType<azdata.SimpleExecuteResult>();
executeMock.setup(x => x.rows).returns(() => rows);
executeMock.setup((x: any) => x.then).returns(() => undefined);
const providerMock = TypeMoq.Mock.ofType<azdata.QueryProvider>();
providerMock.setup(x => x.runQueryAndReturn(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(async () => executeMock.object);
providerMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.dataprotocol, 'getProvider').returns(providerMock.object);
await postgresModel.getEngineSettings();
should(postgresModel.workerNodesEngineSettings.pop()).be.match(engineSettingsModelCompare);
});
});
});

View File

@@ -1,163 +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 { PGResourceInfo, ResourceType } from 'arc';
import * as azdataExt from 'azdata-ext';
import * as should from 'should';
import * as sinon from 'sinon';
import * as TypeMoq from 'typemoq';
import * as vscode from 'vscode';
import { createModelViewMock } from '@microsoft/azdata-test/out/mocks/modelView/modelViewMock';
import { ControllerModel, Registration } from '../../../models/controllerModel';
import { PostgresModel } from '../../../models/postgresModel';
import { PostgresConnectionStringsPage } from '../../../ui/dashboards/postgres/postgresConnectionStringsPage';
import { AzureArcTreeDataProvider } from '../../../ui/tree/azureArcTreeDataProvider';
import { FakeControllerModel } from '../../mocks/fakeControllerModel';
import { FakeAzdataApi } from '../../mocks/fakeAzdataApi';
export const FakePostgresServerShowOutput: azdataExt.AzdataOutput<azdataExt.PostgresServerShowResult> = {
logs: [],
stdout: [],
stderr: [],
result: {
apiVersion: 'version',
kind: 'postgresql',
metadata: {
creationTimestamp: '',
generation: 1,
name: 'pgt',
namespace: 'ns',
resourceVersion: '',
selfLink: '',
uid: '',
},
spec: {
engine: {
extensions: [{ name: '' }],
settings: {
default: { ['']: '' }
},
version: '12'
},
scale: {
shards: 0,
workers: 0
},
scheduling: {
default: {
resources: {
requests: {
cpu: '',
memory: ''
},
limits: {
cpu: '',
memory: ''
}
}
}
},
service: {
type: '',
port: 0
},
storage: {
data: {
className: '',
size: ''
},
logs: {
className: '',
size: ''
},
backups: {
className: '',
size: ''
}
}
},
status: {
externalEndpoint: '127.0.0.1:5432',
readyPods: '',
state: '',
logSearchDashboard: '',
metricsDashboard: '',
podsStatus: [{
conditions: [{
lastTransitionTime: '',
message: '',
reason: '',
status: '',
type: '',
}],
name: '',
role: '',
}]
}
}
};
describe('postgresConnectionStringsPage', function (): void {
let controllerModel: ControllerModel;
let postgresModel: PostgresModel;
let azdataApi: azdataExt.IAzdataApi;
let postgresConnectionStrings: PostgresConnectionStringsPage;
afterEach(function (): void {
sinon.restore();
});
beforeEach(async () => {
// Stub the azdata CLI API
azdataApi = new FakeAzdataApi();
const azdataExt = TypeMoq.Mock.ofType<azdataExt.IExtension>();
azdataExt.setup(x => x.azdata).returns(() => azdataApi);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExt.object });
// Setup Controller Model
controllerModel = new FakeControllerModel();
//Stub calling azdata login and acquiring session
sinon.stub(controllerModel, 'login').returns(Promise.resolve());
// Setup PostgresModel
const postgresResource: PGResourceInfo = { name: 'pgt', resourceType: '' };
const registration: Registration = { instanceName: '', state: '', instanceType: ResourceType.postgresInstances };
postgresModel = new PostgresModel(controllerModel, postgresResource, registration, new AzureArcTreeDataProvider(TypeMoq.Mock.ofType<vscode.ExtensionContext>().object));
// Setup stub of show call
const postgresShow = sinon.stub().returns(FakePostgresServerShowOutput);
sinon.stub(azdataApi, 'arc').get(() => {
return { postgres: { server: { show(name: string) { return postgresShow(name); } } } };
});
// Setup the PostgresConnectionsStringsPage
let { modelViewMock } = createModelViewMock();
postgresConnectionStrings = new PostgresConnectionStringsPage(modelViewMock.object, undefined!, postgresModel);
});
describe('getConnectionStrings', function (): void {
it('Strings container should be empty since postgres model has not been refreshed', async function (): Promise<void> {
should(postgresConnectionStrings['getConnectionStrings']()).be.empty();
});
it('String contain correct ip and port', async function (): Promise<void> {
// Call to provide external endpoint
await postgresModel.refresh();
let endpoint = FakePostgresServerShowOutput.result.status.externalEndpoint.split(':');
postgresConnectionStrings['getConnectionStrings']().forEach(k => {
should(k.value.includes(endpoint[0])).be.True();
should(k.value.includes(endpoint[1])).be.True();
});
});
});
});

View File

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

View File

@@ -18,8 +18,8 @@ describe('ConnectControllerDialog', function (): void {
(<{ info: ControllerInfo | undefined, description: string }[]>[
{ info: undefined, description: 'all input' },
{ info: { endpoint: '127.0.0.1' }, description: 'all but URL' },
{ info: { endpoint: '127.0.0.1', username: 'sa' }, description: 'all but URL and password' }]).forEach(test => {
{ info: { url: '127.0.0.1' }, description: 'all but URL' },
{ info: { url: '127.0.0.1', username: 'sa' }, description: 'all but URL and password' }]).forEach(test => {
it(`Validate returns false when ${test.description} is empty`, async function (): Promise<void> {
const connectControllerDialog = new ConnectToControllerDialog(undefined!);
connectControllerDialog.showDialog(test.info, undefined);
@@ -32,7 +32,7 @@ describe('ConnectControllerDialog', function (): void {
it('validate returns false if controller refresh fails', async function (): Promise<void> {
sinon.stub(ControllerModel.prototype, 'refresh').returns(Promise.reject('Controller refresh failed'));
const connectControllerDialog = new ConnectToControllerDialog(undefined!);
const info: ControllerInfo = { id: uuid(), endpoint: 'https://127.0.0.1:30080', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] };
const info = { id: uuid(), url: 'https://127.0.0.1:30080', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] };
connectControllerDialog.showDialog(info, 'pwd');
await connectControllerDialog.isInitialized;
const validateResult = await connectControllerDialog.validate();
@@ -41,36 +41,36 @@ describe('ConnectControllerDialog', function (): void {
it('validate replaces http with https', async function (): Promise<void> {
await validateConnectControllerDialog(
{ id: uuid(), endpoint: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
{ id: uuid(), url: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
'https://127.0.0.1:30081');
});
it('validate appends https if missing', async function (): Promise<void> {
await validateConnectControllerDialog({ id: uuid(), endpoint: '127.0.0.1:30080', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
await validateConnectControllerDialog({ id: uuid(), url: '127.0.0.1:30080', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
'https://127.0.0.1:30080');
});
it('validate appends default port if missing', async function (): Promise<void> {
await validateConnectControllerDialog({ id: uuid(), endpoint: 'https://127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
await validateConnectControllerDialog({ id: uuid(), url: 'https://127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
'https://127.0.0.1:30080');
});
it('validate appends both port and https if missing', async function (): Promise<void> {
await validateConnectControllerDialog({ id: uuid(), endpoint: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
await validateConnectControllerDialog({ id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
'https://127.0.0.1:30080');
});
for (const name of ['', undefined]) {
it.skip(`validate display name gets set to arc instance name for user chosen name of:${name}`, async function (): Promise<void> {
await validateConnectControllerDialog(
{ id: uuid(), endpoint: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: name!, namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
{ id: uuid(), url: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: name!, username: 'sa', rememberPassword: true, resources: [] },
'https://127.0.0.1:30081');
});
}
it.skip(`validate display name gets set to default data controller name for user chosen name of:'' and instanceName in explicably returned as undefined from the controller endpoint`, async function (): Promise<void> {
await validateConnectControllerDialog(
{ id: uuid(), endpoint: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: '', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
{ id: uuid(), url: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: '', username: 'sa', rememberPassword: true, resources: [] },
'https://127.0.0.1:30081',
undefined);
});
@@ -92,6 +92,6 @@ async function validateConnectControllerDialog(info: ControllerInfo, expectedUrl
const validateResult = await connectControllerDialog.validate();
should(validateResult).be.true('Validation should have returned true');
const model = await connectControllerDialog.waitForClose();
should(model?.controllerModel.info.endpoint).equal(expectedUrl);
should(model?.controllerModel.info.url).equal(expectedUrl);
should(model?.controllerModel.info.name).equal(expectedControllerInfoName);
}

View File

@@ -24,20 +24,6 @@ interface ExtensionGlobalMemento extends vscode.Memento {
setKeysForSync(keys: string[]): void;
}
function getDefaultControllerInfo(): ControllerInfo {
return {
id: uuid(),
endpoint: '127.0.0.1',
kubeConfigFilePath: '/path/to/.kube/config',
kubeClusterContext: 'currentCluster',
username: 'sa',
name: 'my-arc',
namespace: 'arc-ns',
rememberPassword: true,
resources: []
};
}
describe('AzureArcTreeDataProvider tests', function (): void {
let treeDataProvider: AzureArcTreeDataProvider;
beforeEach(function (): void {
@@ -72,7 +58,7 @@ describe('AzureArcTreeDataProvider tests', function (): void {
treeDataProvider['_loading'] = false;
let children = await treeDataProvider.getChildren();
should(children.length).equal(0, 'There initially shouldn\'t be any children');
const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo());
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
await treeDataProvider.addOrUpdateController(controllerModel, '');
should(children.length).equal(1, 'Controller node should be added correctly');
await treeDataProvider.addOrUpdateController(controllerModel, '');
@@ -83,12 +69,12 @@ describe('AzureArcTreeDataProvider tests', function (): void {
treeDataProvider['_loading'] = false;
let children = await treeDataProvider.getChildren();
should(children.length).equal(0, 'There initially shouldn\'t be any children');
const originalInfo: ControllerInfo = getDefaultControllerInfo();
const originalInfo: ControllerInfo = { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] };
const controllerModel = new ControllerModel(treeDataProvider, originalInfo);
await treeDataProvider.addOrUpdateController(controllerModel, '');
should(children.length).equal(1, 'Controller node should be added correctly');
should((<ControllerTreeNode>children[0]).model.info).deepEqual(originalInfo);
const newInfo: ControllerInfo = { id: originalInfo.id, endpoint: '1.1.1.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'new-name', namespace: 'new-namespace', username: 'admin', rememberPassword: false, resources: [] };
const newInfo = { id: originalInfo.id, url: '1.1.1.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'new-name', username: 'admin', rememberPassword: false, resources: [] };
const controllerModel2 = new ControllerModel(treeDataProvider, newInfo);
await treeDataProvider.addOrUpdateController(controllerModel2, '');
should(children.length).equal(1, 'Shouldn\'t add duplicate controller node');
@@ -116,20 +102,18 @@ describe('AzureArcTreeDataProvider tests', function (): void {
return mockArcApi.object;
});
const fakeAzdataApi = new FakeAzdataApi();
const pgInstances = [{ name: 'pg1', state: '', workers: 0 }];
const miaaInstances = [{ name: 'miaa1', state: '', replicas: '', serverEndpoint: '' }];
fakeAzdataApi.postgresInstances = pgInstances;
fakeAzdataApi.miaaInstances = miaaInstances;
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);
sinon.stub(kubeUtils, 'getKubeConfigClusterContexts').returns([{ name: 'currentCluster', isCurrentContext: true }]);
const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo(), 'mypassword');
sinon.stub(kubeUtils, 'getKubeConfigClusterContexts').resolves([{ name: 'currentCluster', isCurrentContext: true }]);
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', 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 === pgInstances[0].name).length).equal(1, 'Should have a Postgres child');
should(children.filter(c => c.label === miaaInstances[0].name).length).equal(1, 'Should have a MIAA child');
should(children.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 exactly 2 children');
});
});
@@ -137,10 +121,8 @@ describe('AzureArcTreeDataProvider tests', function (): void {
describe('removeController', function (): void {
it('removing a controller should work as expected', async function (): Promise<void> {
treeDataProvider['_loading'] = false;
const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo());
const info2 = getDefaultControllerInfo();
info2.username = 'cloudsa';
const controllerModel2 = new ControllerModel(treeDataProvider, info2);
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
const controllerModel2 = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.2', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'cloudsa', rememberPassword: true, resources: [] });
await treeDataProvider.addOrUpdateController(controllerModel, '');
await treeDataProvider.addOrUpdateController(controllerModel2, '');
const children = <ControllerTreeNode[]>(await treeDataProvider.getChildren());
@@ -157,20 +139,20 @@ describe('AzureArcTreeDataProvider tests', function (): void {
describe('openResourceDashboard', function (): void {
it('Opening dashboard for nonexistent controller node throws', async function (): Promise<void> {
const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo());
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', 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, getDefaultControllerInfo());
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', 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, getDefaultControllerInfo());
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', 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)!;

View File

@@ -37,8 +37,7 @@ declare module 'arc' {
id: string,
kubeConfigFilePath: string,
kubeClusterContext: string
endpoint: string | undefined,
namespace: string,
url: string,
name: string,
username: string,
rememberPassword: boolean,

View File

@@ -7,7 +7,7 @@ import * as azdata from 'azdata';
export abstract class Dashboard {
protected dashboard!: azdata.window.ModelViewDashboard;
private dashboard!: azdata.window.ModelViewDashboard;
constructor(protected title: string, protected readonly name: string) { }
@@ -16,10 +16,6 @@ export abstract class Dashboard {
await this.dashboard.open();
}
public async closeDashboard(): Promise<void> {
await this.dashboard.close();
}
protected createDashboard(): azdata.window.ModelViewDashboard {
const dashboard = azdata.window.createModelViewDashboard(this.title, this.name);
dashboard.registerTabs(async modelView => {

View File

@@ -11,7 +11,7 @@ export abstract class DashboardPage extends InitializingComponent {
protected disposables: vscode.Disposable[] = [];
constructor(protected modelView: azdata.ModelView, protected dashboard: azdata.window.ModelViewDashboard) {
constructor(protected modelView: azdata.ModelView) {
super();
this.disposables.push(modelView.onClosed(() => {
// Clean up best we can

View File

@@ -17,9 +17,6 @@ export class RadioOptionsGroup {
private _loadingBuilder: azdata.LoadingComponentBuilder;
private _currentRadioOption!: azdata.RadioButtonComponent;
private _onRadioOptionChanged: vscode.EventEmitter<string | undefined> = new vscode.EventEmitter<string | undefined>();
public onRadioOptionChanged: vscode.Event<string | undefined> = this._onRadioOptionChanged.event;
constructor(private _modelBuilder: azdata.ModelBuilder, private _onNewDisposableCreated: (disposable: vscode.Disposable) => void, private _groupName: string = `RadioOptionsGroup${RadioOptionsGroup.id++}`) {
this._divContainer = this._modelBuilder.divContainer().withProperties<azdata.DivContainerProperties>({ clickable: false }).component();
this._loadingBuilder = this._modelBuilder.loadingComponent().withItem(this._divContainer);
@@ -29,7 +26,7 @@ export class RadioOptionsGroup {
return this._loadingBuilder.component();
}
async load(optionsInfoGetter: () => RadioOptionsInfo | Promise<RadioOptionsInfo>): Promise<void> {
async load(optionsInfoGetter: () => Promise<RadioOptionsInfo>): Promise<void> {
this.component().loading = true;
this._divContainer.clearItems();
try {
@@ -54,7 +51,6 @@ export class RadioOptionsGroup {
// it is just better to keep things clean.
this._currentRadioOption.checked = false;
this._currentRadioOption = radioOption;
this._onRadioOptionChanged.fire(this.value);
}
}));
this._divContainer.addItem(radioOption);

View File

@@ -22,7 +22,7 @@ export class ControllerDashboard extends Dashboard {
}
protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> {
const overviewPage = new ControllerDashboardOverviewPage(modelView, this.dashboard, this._controllerModel);
const overviewPage = new ControllerDashboardOverviewPage(modelView, this._controllerModel);
return [
overviewPage.tab
];

View File

@@ -35,8 +35,8 @@ export class ControllerDashboardOverviewPage extends DashboardPage {
instanceNamespace: '-',
};
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _controllerModel: ControllerModel) {
super(modelView, dashboard);
constructor(modelView: azdata.ModelView, private _controllerModel: ControllerModel) {
super(modelView);
this._azurecoreApi = vscode.extensions.getExtension(azurecore.extension.name)?.exports;

View File

@@ -32,8 +32,8 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
private readonly _azdataApi: azdataExt.IExtension;
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _miaaModel: MiaaModel) {
super(modelView, dashboard);
constructor(protected modelView: azdata.ModelView, private _miaaModel: MiaaModel) {
super(modelView);
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
this.initializeConfigurationBoxes();
@@ -129,18 +129,19 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
cancellable: false
},
async (_progress, _token): Promise<void> => {
let session: azdataExt.AzdataSession | undefined = undefined;
try {
session = await this._miaaModel.controllerModel.acquireAzdataSession();
await this._azdataApi.azdata.arc.sql.mi.edit(
this._miaaModel.info.name, this.saveArgs, this._miaaModel.controllerModel.azdataAdditionalEnvVars, this._miaaModel.controllerModel.controllerContext);
this._miaaModel.info.name, this.saveArgs, this._miaaModel.controllerModel.azdataAdditionalEnvVars, session);
} catch (err) {
this.saveButton!.enabled = true;
throw err;
} finally {
session?.dispose();
}
try {
await this._miaaModel.refresh();
} catch (error) {
vscode.window.showErrorMessage(loc.refreshFailed(error));
}
await this._miaaModel.refresh();
}
);

View File

@@ -8,6 +8,7 @@ import * as loc from '../../../localizedConstants';
import { IconPathHelper, cssStyles } from '../../../constants';
import { KeyValueContainer, KeyValue, InputKeyValue, MultilineInputKeyValue } from '../../components/keyValueContainer';
import { DashboardPage } from '../../components/dashboardPage';
import { ControllerModel } from '../../../models/controllerModel';
import { MiaaModel } from '../../../models/miaaModel';
import { parseIpAndPort } from '../../../common/utils';
@@ -16,9 +17,9 @@ export class MiaaConnectionStringsPage extends DashboardPage {
private _keyValueContainer!: KeyValueContainer;
private _connectionStringsMessage!: azdata.TextComponent;
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _miaaModel: MiaaModel) {
super(modelView, dashboard);
this.disposables.push(this._miaaModel.onConfigUpdated(_ =>
constructor(modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) {
super(modelView);
this.disposables.push(this._controllerModel.onRegistrationsUpdated(_ =>
this.eventuallyRunOnInitialized(() => this.updateConnectionStrings())));
}

View File

@@ -26,9 +26,9 @@ export class MiaaDashboard extends Dashboard {
}
protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> {
const overviewPage = new MiaaDashboardOverviewPage(modelView, this.dashboard, this._controllerModel, this._miaaModel);
const connectionStringsPage = new MiaaConnectionStringsPage(modelView, this.dashboard, this._miaaModel);
const computeAndStoragePage = new MiaaComputeAndStoragePage(modelView, this.dashboard, this._miaaModel);
const overviewPage = new MiaaDashboardOverviewPage(modelView, this._controllerModel, this._miaaModel);
const connectionStringsPage = new MiaaConnectionStringsPage(modelView, this._controllerModel, this._miaaModel);
const computeAndStoragePage = new MiaaComputeAndStoragePage(modelView, this._miaaModel);
return [
overviewPage.tab,
{

View File

@@ -48,8 +48,8 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
vCores: ''
};
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) {
super(modelView, dashboard);
constructor(modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) {
super(modelView);
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
this._azurecoreApi = vscode.extensions.getExtension(azurecore.extension.name)?.exports;
@@ -244,18 +244,17 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
cancellable: false
},
async (_progress, _token) => {
return await this._azdataApi.azdata.arc.sql.mi.delete(this._miaaModel.info.name, this._controllerModel.azdataAdditionalEnvVars, this._controllerModel.controllerContext);
const session = await this._controllerModel.acquireAzdataSession();
try {
return await this._azdataApi.azdata.arc.sql.mi.delete(this._miaaModel.info.name, this._controllerModel.azdataAdditionalEnvVars, session);
} finally {
session.dispose();
}
}
);
await this._controllerModel.refreshTreeNode();
vscode.window.showInformationMessage(loc.instanceDeleted(this._miaaModel.info.name));
try {
await this.dashboard.close();
} catch (err) {
// Failures closing the dashboard aren't something we need to show users
console.log('Error closing MIAA dashboard ', err);
}
}
} catch (error) {
vscode.window.showErrorMessage(loc.instanceDeletionFailed(this._miaaModel.info.name, error));

View File

@@ -47,8 +47,8 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
private readonly _azdataApi: azdataExt.IExtension;
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _postgresModel: PostgresModel) {
super(modelView, dashboard);
constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) {
super(modelView);
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
this.initializeConfigurationBoxes();
@@ -74,16 +74,16 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
const content = this.modelView.modelBuilder.divContainer().component();
root.addItem(content, { CSSStyles: { 'margin': '20px' } });
content.addItem(this.modelView.modelBuilder.text().withProps({
content.addItem(this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: loc.computeAndStorage,
CSSStyles: { ...cssStyles.title }
}).component());
const infoComputeStorage_p1 = this.modelView.modelBuilder.text().withProps({
const infoComputeStorage_p1 = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: loc.postgresComputeAndStorageDescriptionPartOne,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px', 'max-width': 'auto' }
}).component();
const infoComputeStorage_p2 = this.modelView.modelBuilder.text().withProps({
const infoComputeStorage_p2 = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: loc.postgresComputeAndStorageDescriptionPartTwo,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
@@ -94,7 +94,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
const infoComputeStorage_p3 = this.modelView.modelBuilder.text().withProps({
const infoComputeStorage_p3 = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: loc.computeAndStorageDescriptionPartThree,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
@@ -105,17 +105,17 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
const infoComputeStorage_p4 = this.modelView.modelBuilder.text().withProps({
const infoComputeStorage_p4 = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: loc.computeAndStorageDescriptionPartFour,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
const infoComputeStorage_p5 = this.modelView.modelBuilder.text().withProps({
const infoComputeStorage_p5 = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: loc.computeAndStorageDescriptionPartFive,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
const infoComputeStorage_p6 = this.modelView.modelBuilder.text().withProps({
const infoComputeStorage_p6 = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: loc.computeAndStorageDescriptionPartSix,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
@@ -137,7 +137,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
// Worker nodes section
this.workerContainer = this.modelView.modelBuilder.divContainer().component();
this.workerContainer.addItem(this.modelView.modelBuilder.text().withProps({
this.workerContainer.addItem(this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: loc.workerNodes,
CSSStyles: { ...cssStyles.title, 'margin-top': '25px' }
}).component());
@@ -146,7 +146,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
// Coordinator node section
this.coordinatorContainer = this.modelView.modelBuilder.divContainer().component();
this.coordinatorContainer.addItem(this.modelView.modelBuilder.text().withProps({
this.coordinatorContainer.addItem(this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: loc.coordinatorNode,
CSSStyles: { ...cssStyles.title, 'margin-top': '25px' }
}).component());
@@ -162,7 +162,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
protected get toolbarContainer(): azdata.ToolbarContainer {
// Save Edits
this.saveButton = this.modelView.modelBuilder.button().withProps({
this.saveButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
label: loc.saveText,
iconPath: IconPathHelper.save,
enabled: false
@@ -170,7 +170,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
this.disposables.push(
this.saveButton.onDidClick(async () => {
this.saveButton.enabled = false;
this.saveButton!.enabled = false;
try {
await vscode.window.withProgress(
{
@@ -179,7 +179,9 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
cancellable: false
},
async (_progress, _token): Promise<void> => {
let session: azdataExt.AzdataSession | undefined = undefined;
try {
session = await this._postgresModel.controllerModel.acquireAzdataSession();
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name,
{
@@ -189,7 +191,10 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
memoryRequest: this.saveArgs.workerMemoryRequest,
memoryLimit: this.saveArgs.workerMemoryLimit
},
this._postgresModel.controllerModel.azdataAdditionalEnvVars);
this._postgresModel.engineVersion,
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
session
);
/* TODO add second edit call for coordinator configuration
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name,
@@ -199,6 +204,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
memoryRequest: this.saveArgs.coordinatorMemoryRequest,
memoryLimit: this.saveArgs.coordinatorMemoryLimit
},
this._postgresModel.engineVersion,
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
session
);
@@ -206,20 +212,18 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
} catch (err) {
// If an error occurs while editing the instance then re-enable the save button since
// the edit wasn't successfully applied
this.saveButton.enabled = true;
this.saveButton!.enabled = true;
throw err;
} finally {
session?.dispose();
}
try {
await this._postgresModel.refresh();
} catch (error) {
vscode.window.showErrorMessage(loc.refreshFailed(error));
}
await this._postgresModel.refresh();
}
);
vscode.window.showInformationMessage(loc.instanceUpdated(this._postgresModel.info.name));
this.discardButton.enabled = false;
this.discardButton!.enabled = false;
} catch (error) {
vscode.window.showErrorMessage(loc.instanceUpdateFailed(this._postgresModel.info.name, error));
@@ -227,7 +231,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
}));
// Discard
this.discardButton = this.modelView.modelBuilder.button().withProps({
this.discardButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
label: loc.discardText,
iconPath: IconPathHelper.discard,
enabled: false
@@ -235,17 +239,17 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
this.disposables.push(
this.discardButton.onDidClick(async () => {
this.discardButton.enabled = false;
this.discardButton!.enabled = false;
try {
this.workerBox.value = this.currentConfiguration.workers!.toString();
this.workerCoresRequestBox.value = this.currentConfiguration.workerCoresRequest;
this.workerCoresLimitBox.value = this.currentConfiguration.workerCoresLimit;
this.workerMemoryRequestBox.value = this.currentConfiguration.workerMemoryRequest;
this.workerMemoryLimitBox.value = this.currentConfiguration.workerMemoryLimit;
this.workerBox!.value = this.currentConfiguration.workers!.toString();
this.workerCoresRequestBox!.value = this.currentConfiguration.workerCoresRequest;
this.workerCoresLimitBox!.value = this.currentConfiguration.workerCoresLimit;
this.workerMemoryRequestBox!.value = this.currentConfiguration.workerMemoryRequest;
this.workerMemoryLimitBox!.value = this.currentConfiguration.workerMemoryLimit;
} catch (error) {
vscode.window.showErrorMessage(loc.pageDiscardFailed(error));
} finally {
this.saveButton.enabled = false;
this.saveButton!.enabled = false;
}
}));
@@ -257,7 +261,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
private initializeConfigurationBoxes(): void {
// Worker node count
this.workerBox = this.modelView.modelBuilder.inputBox().withProps({
this.workerBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
readOnly: false,
inputType: 'number',
placeHolder: loc.loading,
@@ -266,16 +270,16 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
this.disposables.push(
this.workerBox.onTextChanged(() => {
if (!this.saveValueToEdit(this.workerBox, this.currentConfiguration.workers!.toString())) {
if (!this.saveValueToEdit(this.workerBox!, this.currentConfiguration.workers!.toString())) {
this.saveArgs.workers = undefined;
} else {
this.saveArgs.workers = parseInt(this.workerBox.value!);
this.saveArgs.workers = parseInt(this.workerBox!.value!);
}
})
);
// Worker nodes cores request
this.workerCoresRequestBox = this.modelView.modelBuilder.inputBox().withProps({
this.workerCoresRequestBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
readOnly: false,
min: 1,
inputType: 'number',
@@ -284,18 +288,16 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
this.disposables.push(
this.workerCoresRequestBox.onTextChanged(() => {
if (!(this.saveValueToEdit(this.workerCoresRequestBox, this.currentConfiguration.workerCoresRequest!))) {
if (!(this.saveValueToEdit(this.workerCoresRequestBox!, this.currentConfiguration.workerCoresRequest!))) {
this.saveArgs.workerCoresRequest = undefined;
} else if (this.workerCoresRequestBox.value === '') {
this.saveArgs.workerCoresRequest = '""';
} else {
this.saveArgs.workerCoresRequest = this.workerCoresRequestBox.value;
this.saveArgs.workerCoresRequest = this.workerCoresRequestBox!.value;
}
})
);
// Worker nodes cores limit
this.workerCoresLimitBox = this.modelView.modelBuilder.inputBox().withProps({
this.workerCoresLimitBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
readOnly: false,
min: 1,
inputType: 'number',
@@ -304,18 +306,16 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
this.disposables.push(
this.workerCoresLimitBox.onTextChanged(() => {
if (!(this.saveValueToEdit(this.workerCoresLimitBox, this.currentConfiguration.workerCoresLimit!))) {
if (!(this.saveValueToEdit(this.workerCoresLimitBox!, this.currentConfiguration.workerCoresLimit!))) {
this.saveArgs.workerCoresLimit = undefined;
} else if (this.workerCoresLimitBox.value === '') {
this.saveArgs.workerCoresLimit = '""';
} else {
this.saveArgs.workerCoresLimit = this.workerCoresLimitBox.value;
this.saveArgs.workerCoresLimit = this.workerCoresLimitBox!.value;
}
})
);
// Worker nodes memory request
this.workerMemoryRequestBox = this.modelView.modelBuilder.inputBox().withProps({
this.workerMemoryRequestBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
readOnly: false,
min: 0.25,
inputType: 'number',
@@ -324,18 +324,18 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
this.disposables.push(
this.workerMemoryRequestBox.onTextChanged(() => {
if (!(this.saveValueToEdit(this.workerMemoryRequestBox, this.currentConfiguration.workerMemoryRequest!))) {
if (!(this.saveValueToEdit(this.workerMemoryRequestBox!, this.currentConfiguration.workerMemoryRequest!))) {
this.saveArgs.workerMemoryRequest = undefined;
} else if (this.workerMemoryRequestBox.value === '') {
} else if (this.workerMemoryRequestBox!.value === '') {
this.saveArgs.workerMemoryRequest = '""';
} else {
this.saveArgs.workerMemoryRequest = this.workerMemoryRequestBox.value + 'Gi';
this.saveArgs.workerMemoryRequest = this.workerMemoryRequestBox!.value + 'Gi';
}
})
);
// Worker nodes memory limit
this.workerMemoryLimitBox = this.modelView.modelBuilder.inputBox().withProps({
this.workerMemoryLimitBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
readOnly: false,
min: 0.25,
inputType: 'number',
@@ -344,18 +344,18 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
this.disposables.push(
this.workerMemoryLimitBox.onTextChanged(() => {
if (!(this.saveValueToEdit(this.workerMemoryLimitBox, this.currentConfiguration.workerMemoryLimit!))) {
if (!(this.saveValueToEdit(this.workerMemoryLimitBox!, this.currentConfiguration.workerMemoryLimit!))) {
this.saveArgs.workerMemoryLimit = undefined;
} else if (this.workerMemoryLimitBox.value === '') {
this.saveArgs.workerMemoryLimit = '""';
} else if (this.workerMemoryLimitBox!.value === '""') {
this.saveArgs.workerMemoryLimit = this.workerMemoryLimitBox!.value;
} else {
this.saveArgs.workerMemoryLimit = this.workerMemoryLimitBox.value + 'Gi';
this.saveArgs.workerMemoryLimit = this.workerMemoryLimitBox!.value + 'Gi';
}
})
);
// Coordinator node cores request
this.coordinatorCoresRequestBox = this.modelView.modelBuilder.inputBox().withProps({
this.coordinatorCoresRequestBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
readOnly: false,
min: 1,
inputType: 'number',
@@ -364,18 +364,16 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
this.disposables.push(
this.coordinatorCoresRequestBox.onTextChanged(() => {
if (!(this.saveValueToEdit(this.coordinatorCoresRequestBox, this.currentConfiguration.coordinatorCoresRequest!))) {
if (!(this.saveValueToEdit(this.coordinatorCoresRequestBox!, this.currentConfiguration.coordinatorCoresRequest!))) {
this.saveArgs.coordinatorCoresRequest = undefined;
} else if (this.coordinatorCoresRequestBox.value === '') {
this.saveArgs.coordinatorCoresRequest = '""';
} else {
this.saveArgs.coordinatorCoresRequest = this.coordinatorCoresRequestBox.value;
this.saveArgs.coordinatorCoresRequest = this.coordinatorCoresRequestBox!.value;
}
})
);
// Coordinator node cores limit
this.coordinatorCoresLimitBox = this.modelView.modelBuilder.inputBox().withProps({
this.coordinatorCoresLimitBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
readOnly: false,
min: 1,
inputType: 'number',
@@ -384,18 +382,16 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
this.disposables.push(
this.coordinatorCoresLimitBox.onTextChanged(() => {
if (!(this.saveValueToEdit(this.coordinatorCoresLimitBox, this.currentConfiguration.coordinatorCoresLimit!))) {
if (!(this.saveValueToEdit(this.coordinatorCoresLimitBox!, this.currentConfiguration.coordinatorCoresLimit!))) {
this.saveArgs.coordinatorCoresLimit = undefined;
} else if (this.coordinatorCoresLimitBox.value === '') {
this.saveArgs.coordinatorCoresLimit = '""';
} else {
this.saveArgs.coordinatorCoresLimit = this.coordinatorCoresLimitBox.value;
this.saveArgs.coordinatorCoresLimit = this.coordinatorCoresLimitBox!.value;
}
})
);
// Coordinator node memory request
this.coordinatorMemoryRequestBox = this.modelView.modelBuilder.inputBox().withProps({
this.coordinatorMemoryRequestBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
readOnly: false,
min: 0.25,
inputType: 'number',
@@ -404,18 +400,18 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
this.disposables.push(
this.coordinatorMemoryRequestBox.onTextChanged(() => {
if (!(this.saveValueToEdit(this.coordinatorMemoryRequestBox, this.currentConfiguration.coordinatorMemoryRequest!))) {
if (!(this.saveValueToEdit(this.coordinatorMemoryRequestBox!, this.currentConfiguration.coordinatorMemoryRequest!))) {
this.saveArgs.coordinatorMemoryRequest = undefined;
} else if (this.coordinatorMemoryRequestBox.value === '') {
} else if (this.coordinatorMemoryRequestBox!.value === '') {
this.saveArgs.coordinatorMemoryRequest = '""';
} else {
this.saveArgs.coordinatorMemoryRequest = this.coordinatorMemoryRequestBox.value + 'Gi';
this.saveArgs.coordinatorMemoryRequest = this.coordinatorMemoryRequestBox!.value + 'Gi';
}
})
);
// Coordinator node memory limit
this.coordinatorMemoryLimitBox = this.modelView.modelBuilder.inputBox().withProps({
this.coordinatorMemoryLimitBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
readOnly: false,
min: 0.25,
inputType: 'number',
@@ -424,12 +420,12 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
this.disposables.push(
this.coordinatorMemoryLimitBox.onTextChanged(() => {
if (!(this.saveValueToEdit(this.coordinatorMemoryLimitBox, this.currentConfiguration.coordinatorMemoryLimit!))) {
if (!(this.saveValueToEdit(this.coordinatorMemoryLimitBox!, this.currentConfiguration.coordinatorMemoryLimit!))) {
this.saveArgs.coordinatorMemoryLimit = undefined;
} else if (this.coordinatorMemoryLimitBox.value === '') {
} else if (this.coordinatorMemoryLimitBox!.value === '') {
this.saveArgs.coordinatorMemoryLimit = '""';
} else {
this.saveArgs.coordinatorMemoryLimit = this.coordinatorMemoryLimitBox.value + 'Gi';
this.saveArgs.coordinatorMemoryLimit = this.coordinatorMemoryLimitBox!.value + 'Gi';
}
})
);
@@ -445,10 +441,10 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
return [
this.createWorkerNodesSectionContainer(),
this.createCoresMemorySection(loc.configurationPerNode, loc.postgresConfigurationInformation), // use loc.workerNodesConfigurationInformation when coordinator section is included
this.createConfigurationSectionContainer(loc.coresRequest, this.workerCoresRequestBox),
this.createConfigurationSectionContainer(loc.coresLimit, this.workerCoresLimitBox),
this.createConfigurationSectionContainer(loc.memoryRequest, this.workerMemoryRequestBox),
this.createConfigurationSectionContainer(loc.memoryLimit, this.workerMemoryLimitBox)
this.createConfigurationSectionContainer(loc.coresRequest, this.workerCoresRequestBox!),
this.createConfigurationSectionContainer(loc.coresLimit, this.workerCoresLimitBox!),
this.createConfigurationSectionContainer(loc.memoryRequest, this.workerMemoryRequestBox!),
this.createConfigurationSectionContainer(loc.memoryLimit, this.workerMemoryLimitBox!)
];
}
@@ -462,7 +458,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
alignItems: 'center'
}).component();
const keyComponent = this.modelView.modelBuilder.text().withProps({
const keyComponent = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: loc.workerNodeCount,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
@@ -470,7 +466,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
const keyContainer = this.modelView.modelBuilder.flexContainer().withLayout({ alignItems: 'center' }).component();
keyContainer.addItem(keyComponent, { CSSStyles: { 'margin-right': '0px', 'margin-bottom': '15px' } });
const information = this.modelView.modelBuilder.button().withProps({
const information = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
iconPath: IconPathHelper.information,
title: loc.workerNodesInformation,
width: '15px',
@@ -482,7 +478,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
flexContainer.addItem(keyContainer, keyFlex);
const inputContainer = this.modelView.modelBuilder.flexContainer().withLayout({ alignItems: 'center' }).component();
inputContainer.addItem(this.workerBox, { CSSStyles: { 'margin-bottom': '15px', 'min-width': '50px', 'max-width': '225px' } });
inputContainer.addItem(this.workerBox!, { CSSStyles: { 'margin-bottom': '15px', 'min-width': '50px', 'max-width': '225px' } });
flexContainer.addItem(inputContainer, inputFlex);
@@ -497,10 +493,10 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
return [
this.createCoresMemorySection(loc.configuration, loc.coordinatorNodeConfigurationInformation),
this.createConfigurationSectionContainer(loc.coresRequest, this.coordinatorCoresRequestBox),
this.createConfigurationSectionContainer(loc.coresLimit, this.coordinatorCoresLimitBox),
this.createConfigurationSectionContainer(loc.memoryRequest, this.coordinatorMemoryRequestBox),
this.createConfigurationSectionContainer(loc.memoryLimit, this.coordinatorMemoryLimitBox)
this.createConfigurationSectionContainer(loc.coresRequest, this.coordinatorCoresRequestBox!),
this.createConfigurationSectionContainer(loc.coresLimit, this.coordinatorCoresLimitBox!),
this.createConfigurationSectionContainer(loc.memoryRequest, this.coordinatorMemoryRequestBox!),
this.createConfigurationSectionContainer(loc.memoryLimit, this.coordinatorMemoryLimitBox!)
];
}
@@ -514,7 +510,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
alignItems: 'center'
}).component();
const keyComponent = this.modelView.modelBuilder.text().withProps({
const keyComponent = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: key,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
@@ -533,7 +529,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
/**
* A function that determines if an input box's value should be considered or not.
* Triggers the save and discard buttons to become enabled depending on the value change.
* Tiggers the save and discard buttons to become enabled depnding on the value change.
*
* If new value is the same as value found in config, do not consider this new value for editing.
* If new value is invalid, do not consider this new value for editing and enable discard button.
@@ -550,8 +546,8 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
} else if ((!component.valid)) {
return false;
} else {
this.saveButton.enabled = true;
this.discardButton.enabled = true;
this.saveButton!.enabled = true;
this.discardButton!.enabled = true;
return true;
}
}
@@ -561,9 +557,9 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
let scale = this._postgresModel.config?.spec.scale;
this.currentConfiguration.workers = scale?.workers ?? scale?.shards ?? 0;
this.workerBox.min = this.currentConfiguration.workers;
this.workerBox.placeHolder = '';
this.workerBox.value = this.currentConfiguration.workers.toString();
this.workerBox!.min = this.currentConfiguration.workers;
this.workerBox!.placeHolder = '';
this.workerBox!.value = this.currentConfiguration.workers.toString();
this.saveArgs.workers = undefined;
}
@@ -575,7 +571,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
alignItems: 'center'
}).component();
const titleComponent = this.modelView.modelBuilder.text().withProps({
const titleComponent = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: title,
CSSStyles: { ...cssStyles.title, 'font-weight': 'bold', 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
@@ -583,7 +579,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
const titleContainer = this.modelView.modelBuilder.flexContainer().withLayout({ alignItems: 'center' }).component();
titleContainer.addItem(titleComponent, { CSSStyles: { 'margin-right': '0px', 'margin-bottom': '15px' } });
const information = this.modelView.modelBuilder.button().withProps({
const information = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
iconPath: IconPathHelper.information,
title: description,
width: '15px',
@@ -607,8 +603,8 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
this.currentConfiguration.workerCoresRequest = '';
}
this.workerCoresRequestBox.placeHolder = '';
this.workerCoresRequestBox.value = this.currentConfiguration.workerCoresRequest;
this.workerCoresRequestBox!.placeHolder = '';
this.workerCoresRequestBox!.value = this.currentConfiguration.workerCoresRequest;
this.saveArgs.workerCoresRequest = undefined;
// Cores Limit
@@ -617,8 +613,8 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
this.currentConfiguration.workerCoresLimit = '';
}
this.workerCoresLimitBox.placeHolder = '';
this.workerCoresLimitBox.value = this.currentConfiguration.workerCoresLimit;
this.workerCoresLimitBox!.placeHolder = '';
this.workerCoresLimitBox!.value = this.currentConfiguration.workerCoresLimit;
this.saveArgs.workerCoresLimit = undefined;
}
@@ -631,8 +627,8 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
this.currentConfiguration.workerMemoryRequest = convertToGibibyteString(currentMemorySize);
}
this.workerMemoryRequestBox.placeHolder = '';
this.workerMemoryRequestBox.value = this.currentConfiguration.workerMemoryRequest;
this.workerMemoryRequestBox!.placeHolder = '';
this.workerMemoryRequestBox!.value = this.currentConfiguration.workerMemoryRequest;
this.saveArgs.workerMemoryRequest = undefined;
//Memory Limit
@@ -643,8 +639,8 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
this.currentConfiguration.workerMemoryLimit = convertToGibibyteString(currentMemorySize);
}
this.workerMemoryLimitBox.placeHolder = '';
this.workerMemoryLimitBox.value = this.currentConfiguration.workerMemoryLimit;
this.workerMemoryLimitBox!.placeHolder = '';
this.workerMemoryLimitBox!.value = this.currentConfiguration.workerMemoryLimit;
this.saveArgs.workerMemoryLimit = undefined;
}
@@ -655,8 +651,8 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
this.currentConfiguration.coordinatorCoresRequest = '';
}
this.coordinatorCoresRequestBox.placeHolder = '';
this.coordinatorCoresRequestBox.value = this.currentConfiguration.coordinatorCoresRequest;
this.coordinatorCoresRequestBox!.placeHolder = '';
this.coordinatorCoresRequestBox!.value = this.currentConfiguration.coordinatorCoresRequest;
this.saveArgs.coordinatorCoresRequest = undefined;
// TODO get current cpu size for coordinator
@@ -665,8 +661,8 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
this.currentConfiguration.coordinatorCoresLimit = '';
}
this.coordinatorCoresLimitBox.placeHolder = '';
this.coordinatorCoresLimitBox.value = this.currentConfiguration.coordinatorCoresLimit;
this.coordinatorCoresLimitBox!.placeHolder = '';
this.coordinatorCoresLimitBox!.value = this.currentConfiguration.coordinatorCoresLimit;
this.saveArgs.coordinatorCoresLimit = undefined;
}
@@ -679,8 +675,8 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
this.currentConfiguration.coordinatorCoresRequest = convertToGibibyteString(currentMemorySize);
}
this.coordinatorMemoryRequestBox.placeHolder = '';
this.coordinatorMemoryRequestBox.value = this.currentConfiguration.coordinatorMemoryRequest;
this.coordinatorMemoryRequestBox!.placeHolder = '';
this.coordinatorMemoryRequestBox!.value = this.currentConfiguration.coordinatorMemoryRequest;
this.saveArgs.coordinatorMemoryRequest = undefined;
// TODO get current memory size for coordinator
@@ -691,8 +687,8 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
this.currentConfiguration.coordinatorCoresLimit = convertToGibibyteString(currentMemorySize);
}
this.coordinatorMemoryLimitBox.placeHolder = '';
this.coordinatorMemoryLimitBox.value = this.currentConfiguration.coordinatorMemoryLimit;
this.coordinatorMemoryLimitBox!.placeHolder = '';
this.coordinatorMemoryLimitBox!.value = this.currentConfiguration.coordinatorMemoryLimit;
this.saveArgs.coordinatorMemoryLimit = undefined;
}

View File

@@ -14,8 +14,8 @@ export class PostgresConnectionStringsPage extends DashboardPage {
private keyValueContainer?: KeyValueContainer;
private connectionStringsLoading!: azdata.LoadingComponent;
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _postgresModel: PostgresModel) {
super(modelView, dashboard);
constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) {
super(modelView);
this.disposables.push(this._postgresModel.onConfigUpdated(
() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())));

View File

@@ -7,12 +7,12 @@ import * as azdata from 'azdata';
import * as loc from '../../../localizedConstants';
import { IconPathHelper } from '../../../constants';
import { PostgresParametersPage } from './postgresParameters';
import { PostgresModel, EngineSettingsModel } from '../../../models/postgresModel';
import { PostgresModel } from '../../../models/postgresModel';
export class PostgresCoordinatorNodeParametersPage extends PostgresParametersPage {
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, postgresModel: PostgresModel) {
super(modelView, dashboard, postgresModel);
constructor(protected modelView: azdata.ModelView, _postgresModel: PostgresModel) {
super(modelView, _postgresModel);
}
protected get title(): string {
@@ -31,15 +31,12 @@ export class PostgresCoordinatorNodeParametersPage extends PostgresParametersPag
return loc.coordinatorNodeParametersDescription;
}
protected get engineSettings(): EngineSettingsModel[] {
return this._postgresModel.coordinatorNodeEngineSettings;
}
protected async saveParameterEdits(): Promise<void> {
/* TODO add correct azdata call for editing coordinator parameters
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name,
{ engineSettings: engineSettings.toString() },
this._postgresModel.engineVersion,
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
session);
*/
@@ -50,6 +47,7 @@ export class PostgresCoordinatorNodeParametersPage extends PostgresParametersPag
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name,
{ engineSettings: `''`, replaceEngineSettings: true },
this._postgresModel.engineVersion,
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
session);
*/
@@ -60,8 +58,14 @@ export class PostgresCoordinatorNodeParametersPage extends PostgresParametersPag
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name,
{ engineSettings: parameterName + '=' },
this._postgresModel.engineVersion,
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
session);
*/
}
protected refreshParametersTable(): void {
this._parameters = this._postgresModel.coordinatorNodeEngineSettings.map(engineSetting => this.createParameterComponents(engineSetting));
this._parametersTable.data = this._parameters.map(p => [p.parameterName, p.valueContainer, p.description, p.resetButton]);
}
}

View File

@@ -16,7 +16,6 @@ import { PostgresSupportRequestPage } from './postgresSupportRequestPage';
import { PostgresComputeAndStoragePage } from './postgresComputeAndStoragePage';
import { PostgresWorkerNodeParametersPage } from './postgresWorkerNodeParametersPage';
import { PostgresPropertiesPage } from './postgresPropertiesPage';
import { PostgresResourceHealthPage } from './postgresResourceHealthPage';
export class PostgresDashboard extends Dashboard {
constructor(private _context: vscode.ExtensionContext, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
@@ -32,16 +31,15 @@ export class PostgresDashboard extends Dashboard {
}
protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> {
const overviewPage = new PostgresOverviewPage(modelView, this.dashboard, this._controllerModel, this._postgresModel);
const connectionStringsPage = new PostgresConnectionStringsPage(modelView, this.dashboard, this._postgresModel);
const computeAndStoragePage = new PostgresComputeAndStoragePage(modelView, this.dashboard, this._postgresModel);
const propertiesPage = new PostgresPropertiesPage(modelView, this.dashboard, this._controllerModel, this._postgresModel);
const overviewPage = new PostgresOverviewPage(modelView, this._controllerModel, this._postgresModel);
const connectionStringsPage = new PostgresConnectionStringsPage(modelView, this._postgresModel);
const computeAndStoragePage = new PostgresComputeAndStoragePage(modelView, this._postgresModel);
const propertiesPage = new PostgresPropertiesPage(modelView, this._controllerModel, this._postgresModel);
// TODO Add dashboard once backend is able to be connected for per role server parameter edits.
// const coordinatorNodeParametersPage = new PostgresCoordinatorNodeParametersPage(modelView, this._postgresModel);
const workerNodeParametersPage = new PostgresWorkerNodeParametersPage(modelView, this.dashboard, this._postgresModel);
const diagnoseAndSolveProblemsPage = new PostgresDiagnoseAndSolveProblemsPage(modelView, this.dashboard, this._context, this._controllerModel, this._postgresModel);
const supportRequestPage = new PostgresSupportRequestPage(modelView, this.dashboard, this._controllerModel, this._postgresModel);
const resourceHealthPage = new PostgresResourceHealthPage(modelView, this.dashboard, this._postgresModel);
const workerNodeParametersPage = new PostgresWorkerNodeParametersPage(modelView, this._postgresModel);
const diagnoseAndSolveProblemsPage = new PostgresDiagnoseAndSolveProblemsPage(modelView, this._context, this._postgresModel);
const supportRequestPage = new PostgresSupportRequestPage(modelView, this._controllerModel, this._postgresModel);
return [
overviewPage.tab,
@@ -57,7 +55,6 @@ export class PostgresDashboard extends Dashboard {
{
title: loc.supportAndTroubleshooting,
tabs: [
resourceHealthPage.tab,
diagnoseAndSolveProblemsPage.tab,
supportRequestPage.tab
]

View File

@@ -9,11 +9,10 @@ import * as loc from '../../../localizedConstants';
import { IconPathHelper, cssStyles } from '../../../constants';
import { DashboardPage } from '../../components/dashboardPage';
import { PostgresModel } from '../../../models/postgresModel';
import { ControllerModel } from '../../../models/controllerModel';
export class PostgresDiagnoseAndSolveProblemsPage extends DashboardPage {
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _context: vscode.ExtensionContext, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
super(modelView, dashboard);
constructor(protected modelView: azdata.ModelView, private _context: vscode.ExtensionContext, private _postgresModel: PostgresModel) {
super(modelView);
}
protected get title(): string {
@@ -51,8 +50,9 @@ export class PostgresDiagnoseAndSolveProblemsPage extends DashboardPage {
this.disposables.push(
troubleshootButton.onDidClick(() => {
process.env['POSTGRES_SERVER_NAMESPACE'] = this._controllerModel.controllerConfig?.metadata.namespace ?? '';
process.env['POSTGRES_SERVER_NAMESPACE'] = this._postgresModel.config?.metadata.namespace;
process.env['POSTGRES_SERVER_NAME'] = this._postgresModel.info.name;
process.env['POSTGRES_SERVER_VERSION'] = this._postgresModel.engineVersion;
vscode.commands.executeCommand('bookTreeView.openBook', this._context.asAbsolutePath('notebooks/arcDataServices'), true, 'postgres/tsg100-troubleshoot-postgres');
}));

View File

@@ -30,15 +30,14 @@ export class PostgresOverviewPage extends DashboardPage {
private properties!: azdata.PropertiesContainerComponent;
private kibanaLink!: azdata.HyperlinkComponent;
private grafanaLink!: azdata.HyperlinkComponent;
private deleteButton!: azdata.ButtonComponent;
private podStatusTable!: azdata.DeclarativeTableComponent;
private podStatusData: PodStatusModel[] = [];
private readonly _azdataApi: azdataExt.IExtension;
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
super(modelView, dashboard);
constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
super(modelView);
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
this.disposables.push(
@@ -217,13 +216,21 @@ export class PostgresOverviewPage extends DashboardPage {
try {
const password = await promptAndConfirmPassword(input => !input ? loc.enterANonEmptyPassword : '');
if (password) {
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name,
{
adminPassword: true,
noWait: true
},
Object.assign({ 'AZDATA_PASSWORD': password }, this._controllerModel.azdataAdditionalEnvVars));
const session = await this._postgresModel.controllerModel.acquireAzdataSession();
try {
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name,
{
adminPassword: true,
noWait: true
},
this._postgresModel.engineVersion,
Object.assign({ 'AZDATA_PASSWORD': password }, this._controllerModel.azdataAdditionalEnvVars),
session
);
} finally {
session.dispose();
}
vscode.window.showInformationMessage(loc.passwordReset);
}
} catch (error) {
@@ -234,14 +241,14 @@ export class PostgresOverviewPage extends DashboardPage {
}));
// Delete service
this.deleteButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
const deleteButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
label: loc.deleteText,
iconPath: IconPathHelper.delete
}).component();
this.disposables.push(
this.deleteButton.onDidClick(async () => {
this.deleteButton.enabled = false;
deleteButton.onDidClick(async () => {
deleteButton.enabled = false;
try {
if (await promptForInstanceDeletion(this._postgresModel.info.name)) {
await vscode.window.withProgress(
@@ -251,23 +258,22 @@ export class PostgresOverviewPage extends DashboardPage {
cancellable: false
},
async (_progress, _token) => {
return await this._azdataApi.azdata.arc.postgres.server.delete(this._postgresModel.info.name, this._controllerModel.azdataAdditionalEnvVars, this._controllerModel.controllerContext);
const session = await this._postgresModel.controllerModel.acquireAzdataSession();
try {
return await this._azdataApi.azdata.arc.postgres.server.delete(this._postgresModel.info.name, this._controllerModel.azdataAdditionalEnvVars, session);
} finally {
session.dispose();
}
}
);
await this._controllerModel.refreshTreeNode();
vscode.window.showInformationMessage(loc.instanceDeleted(this._postgresModel.info.name));
try {
await this.dashboard.close();
} catch (err) {
// Failures closing the dashboard aren't something we need to show users
console.log('Error closing Arc Postgres dashboard ', err);
}
}
} catch (error) {
vscode.window.showErrorMessage(loc.instanceDeletionFailed(this._postgresModel.info.name, error));
} finally {
this.deleteButton.enabled = true;
deleteButton.enabled = true;
}
}));
@@ -317,7 +323,7 @@ export class PostgresOverviewPage extends DashboardPage {
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([
{ component: resetPasswordButton },
{ component: this.deleteButton },
{ component: deleteButton },
{ component: refreshButton, toolbarSeparatorAfter: true },
{ component: openInAzurePortalButton }
]).component();

View File

@@ -15,9 +15,7 @@ import { debounce } from '../../../common/utils';
export type ParametersModel = {
parameterName: string,
originalValue: string,
valueComponent: azdata.TextComponent | azdata.DropDownComponent | azdata.CheckBoxComponent,
information?: azdata.ButtonComponent,
valueContainer: azdata.FlexContainer,
description: string,
resetButton: azdata.ButtonComponent
};
@@ -25,8 +23,8 @@ export type ParametersModel = {
export abstract class PostgresParametersPage extends DashboardPage {
private searchBox!: azdata.InputBoxComponent;
protected _parametersTable!: azdata.DeclarativeTableComponent;
private parameterContainer!: azdata.DivContainer;
private parametersTableLoading?: azdata.LoadingComponent;
private parameterContainer?: azdata.DivContainer;
private parametersTableLoading!: azdata.LoadingComponent;
private discardButton!: azdata.ButtonComponent;
private saveButton!: azdata.ButtonComponent;
@@ -34,16 +32,16 @@ export abstract class PostgresParametersPage extends DashboardPage {
private connectToServerButton?: azdata.ButtonComponent;
protected _parameters: ParametersModel[] = [];
private changedComponentValues: Set<string> = new Set();
private parameterUpdates: Map<string, string> = new Map();
protected readonly _azdataApi: azdataExt.IExtension;
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, protected _postgresModel: PostgresModel) {
super(modelView, dashboard);
constructor(protected modelView: azdata.ModelView, protected _postgresModel: PostgresModel) {
super(modelView);
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
this.initializeConnectButton();
this.initializeSearchBox();
this.disposables.push(
@@ -54,8 +52,6 @@ export abstract class PostgresParametersPage extends DashboardPage {
protected abstract get description(): string;
protected abstract get engineSettings(): EngineSettingsModel[];
protected get container(): azdata.Component {
const root = this.modelView.modelBuilder.divContainer().component();
const content = this.modelView.modelBuilder.divContainer().component();
@@ -119,8 +115,11 @@ export abstract class PostgresParametersPage extends DashboardPage {
data: []
}).component();
this.parametersTableLoading = this.modelView.modelBuilder.loadingComponent().component();
this.parameterContainer = this.modelView.modelBuilder.divContainer().component();
this.selectComponent();
content.addItem(this.parameterContainer);
this.initialized = true;
@@ -139,7 +138,7 @@ export abstract class PostgresParametersPage extends DashboardPage {
let engineSettings: string[] = [];
this.disposables.push(
this.saveButton.onDidClick(async () => {
this.saveButton.enabled = false;
this.saveButton!.enabled = false;
try {
await vscode.window.withProgress(
{
@@ -149,31 +148,31 @@ export abstract class PostgresParametersPage extends DashboardPage {
},
async (_progress, _token): Promise<void> => {
try {
this.parameterUpdates.forEach((value, key) => {
this.parameterUpdates!.forEach((value, key) => {
engineSettings.push(`${key}="${value}"`);
});
await this.saveParameterEdits(engineSettings.toString());
const session = await this._postgresModel.controllerModel.acquireAzdataSession();
try {
this.saveParameterEdits(engineSettings.toString(), session);
} finally {
session.dispose();
}
} catch (err) {
// If an error occurs while editing the instance then re-enable the save button since
// the edit wasn't successfully applied
this.saveButton.enabled = true;
this.saveButton!.enabled = true;
throw err;
}
try {
await this._postgresModel.refresh();
} catch (error) {
vscode.window.showErrorMessage(loc.refreshFailed(error));
}
await this._postgresModel.refresh();
}
);
vscode.window.showInformationMessage(loc.instanceUpdated(this._postgresModel.info.name));
engineSettings = [];
this.changedComponentValues.clear();
this.parameterUpdates.clear();
this.discardButton.enabled = false;
this.resetAllButton.enabled = true;
this.parameterUpdates!.clear();
this.discardButton!.enabled = false;
this.resetAllButton!.enabled = true;
} catch (error) {
vscode.window.showErrorMessage(loc.instanceUpdateFailed(this._postgresModel.info.name, error));
@@ -190,16 +189,13 @@ export abstract class PostgresParametersPage extends DashboardPage {
this.disposables.push(
this.discardButton.onDidClick(async () => {
this.discardButton.enabled = false;
this.discardButton!.enabled = false;
try {
this.discardParametersTableChanges();
this.refreshParametersTable();
} catch (error) {
this.discardButton!.enabled = true;
vscode.window.showErrorMessage(loc.pageDiscardFailed(error));
} finally {
this.changedComponentValues.clear();
this.saveButton.enabled = false;
this.parameterUpdates.clear();
this.saveButton!.enabled = false;
}
})
);
@@ -213,9 +209,9 @@ export abstract class PostgresParametersPage extends DashboardPage {
this.disposables.push(
this.resetAllButton.onDidClick(async () => {
this.resetAllButton.enabled = false;
this.discardButton.enabled = false;
this.saveButton.enabled = false;
this.resetAllButton!.enabled = false;
this.discardButton!.enabled = false;
this.saveButton!.enabled = false;
try {
await vscode.window.withProgress(
{
@@ -224,29 +220,28 @@ export abstract class PostgresParametersPage extends DashboardPage {
cancellable: false
},
async (_progress, _token): Promise<void> => {
const session = await this._postgresModel.controllerModel.acquireAzdataSession();
try {
await this.resetAllParameters();
this.resetAllParameters(session);
} catch (err) {
// If an error occurs while resetting the instance then re-enable the reset button since
// the edit wasn't successfully applied
if (this.parameterUpdates.size > 0) {
this.discardButton.enabled = true;
this.saveButton.enabled = true;
this.discardButton!.enabled = true;
this.saveButton!.enabled = true;
}
this.resetAllButton.enabled = true;
this.resetAllButton!.enabled = true;
throw err;
} finally {
session?.dispose();
}
this.changedComponentValues.clear();
try {
await this._postgresModel.refresh();
} catch (error) {
vscode.window.showErrorMessage(loc.refreshFailed(error));
}
await this._postgresModel.refresh();
}
);
vscode.window.showInformationMessage(loc.instanceUpdated(this._postgresModel.info.name));
this.parameterUpdates.clear();
this.parameterUpdates!.clear();
} catch (error) {
vscode.window.showErrorMessage(loc.resetFailed(error));
@@ -269,7 +264,7 @@ export abstract class PostgresParametersPage extends DashboardPage {
}).component();
this.disposables.push(
this.connectToServerButton.onDidClick(async () => {
this.connectToServerButton!.onDidClick(async () => {
this.connectToServerButton!.enabled = false;
if (!vscode.extensions.getExtension(loc.postgresExtension)) {
const response = await vscode.window.showErrorMessage(loc.missingExtension('PostgreSQL'), loc.yes, loc.no);
@@ -299,28 +294,26 @@ export abstract class PostgresParametersPage extends DashboardPage {
this.parametersTableLoading!.loading = true;
await this.callGetEngineSettings().finally(() => this.parametersTableLoading!.loading = false);
this.searchBox.enabled = true;
this.resetAllButton.enabled = true;
this.parameterContainer.clearItems();
this.parameterContainer.addItem(this._parametersTable);
this.searchBox!.enabled = true;
this.resetAllButton!.enabled = true;
this.parameterContainer!.clearItems();
this.parameterContainer!.addItem(this._parametersTable);
})
);
}
private selectComponent(): void {
if (!this._postgresModel.engineSettingsLastUpdated) {
this.parameterContainer.addItem(this.modelView.modelBuilder.text().withProps({
this.parameterContainer!.addItem(this.modelView.modelBuilder.text().withProps({
value: loc.connectToPostgresDescription,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component());
this.initializeConnectButton();
this.parameterContainer.addItem(this.connectToServerButton!, { CSSStyles: { 'max-width': '125px' } });
this.parametersTableLoading = this.modelView.modelBuilder.loadingComponent().component();
this.parameterContainer.addItem(this.parametersTableLoading);
this.parameterContainer!.addItem(this.connectToServerButton!, { CSSStyles: { 'max-width': '125px' } });
this.parameterContainer!.addItem(this.parametersTableLoading!);
} else {
this.searchBox.enabled = true;
this.resetAllButton.enabled = true;
this.parameterContainer.addItem(this._parametersTable!);
this.searchBox!.enabled = true;
this.resetAllButton!.enabled = true;
this.parameterContainer!.addItem(this._parametersTable!);
this.refreshParametersTable();
}
}
@@ -355,10 +348,10 @@ export abstract class PostgresParametersPage extends DashboardPage {
@debounce(500)
private onSearchFilter(): void {
if (!this.searchBox.value) {
if (!this.searchBox!.value) {
this._parametersTable.setFilter(undefined);
} else {
this.filterParameters(this.searchBox.value);
this.filterParameters(this.searchBox!.value);
}
}
@@ -372,28 +365,137 @@ export abstract class PostgresParametersPage extends DashboardPage {
this._parametersTable.setFilter(filteredRowIndexes);
}
private handleOnTextChanged(component: azdata.InputBoxComponent, name: string, currentValue: string | undefined): boolean {
private handleOnTextChanged(component: azdata.InputBoxComponent, currentValue: string | undefined): boolean {
if (!component.valid) {
// If invalid value return false and enable discard button
this.discardButton.enabled = true;
this.collectChangedComponents(name);
this.discardButton!.enabled = true;
return false;
} else if (component.value === currentValue) {
this.removeFromChangedComponents(name);
return false;
} else {
/* If a valid value has been entered into the input box, enable save and discard buttons
so that user could choose to either edit instance or clear all inputs
return true */
this.saveButton.enabled = true;
this.discardButton.enabled = true;
this.collectChangedComponents(name);
this.saveButton!.enabled = true;
this.discardButton!.enabled = true;
return true;
}
}
protected createParameterComponents(engineSetting: EngineSettingsModel): ParametersModel {
// Container to hold input component and information bubble
const valueContainer = this.modelView.modelBuilder.flexContainer().withLayout({ alignItems: 'center' }).component();
if (engineSetting.type === 'enum') {
// If type is enum, component should be drop down menu
let options = engineSetting.options?.slice(1, -1).split(',');
let values: string[] = [];
options!.forEach(option => {
values.push(option.slice(option.indexOf('"') + 1, -1));
});
let valueBox = this.modelView.modelBuilder.dropDown().withProps({
values: values,
value: engineSetting.value,
width: '150px'
}).component();
valueContainer.addItem(valueBox);
this.disposables.push(
valueBox.onValueChanged(() => {
if (engineSetting.value !== String(valueBox.value)) {
this.parameterUpdates!.set(engineSetting.parameterName!, String(valueBox.value));
this.saveButton!.enabled = true;
this.discardButton!.enabled = true;
} else if (this.parameterUpdates!.has(engineSetting.parameterName!)) {
this.parameterUpdates!.delete(engineSetting.parameterName!);
}
})
);
} else if (engineSetting.type === 'bool') {
// If type is bool, component should be checkbox to turn on or off
let valueBox = this.modelView.modelBuilder.checkBox().withProps({
label: loc.on,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
valueContainer.addItem(valueBox);
if (engineSetting.value === 'on') {
valueBox.checked = true;
} else {
valueBox.checked = false;
}
this.disposables.push(
valueBox.onChanged(() => {
if (valueBox.checked && engineSetting.value === 'off') {
this.parameterUpdates!.set(engineSetting.parameterName!, loc.on);
this.saveButton!.enabled = true;
this.discardButton!.enabled = true;
} else if (!valueBox.checked && engineSetting.value === 'on') {
this.parameterUpdates!.set(engineSetting.parameterName!, loc.off);
this.saveButton!.enabled = true;
this.discardButton!.enabled = true;
} else if (this.parameterUpdates!.has(engineSetting.parameterName!)) {
this.parameterUpdates!.delete(engineSetting.parameterName!);
}
})
);
} else if (engineSetting.type === 'string') {
// If type is string, component should be text inputbox
let valueBox = this.modelView.modelBuilder.inputBox().withProps({
required: true,
readOnly: false,
value: engineSetting.value,
width: '150px'
}).component();
valueContainer.addItem(valueBox);
this.disposables.push(
valueBox.onTextChanged(() => {
if ((this.handleOnTextChanged(valueBox, engineSetting.value))) {
this.parameterUpdates!.set(engineSetting.parameterName!, `"${valueBox.value!}"`);
} else if (this.parameterUpdates!.has(engineSetting.parameterName!)) {
this.parameterUpdates!.delete(engineSetting.parameterName!);
}
})
);
} else {
// If type is real or interger, component should be inputbox set to inputType of number. Max and min values also set.
let valueBox = this.modelView.modelBuilder.inputBox().withProps({
required: true,
readOnly: false,
min: parseInt(engineSetting.min!),
max: parseInt(engineSetting.max!),
inputType: 'number',
value: engineSetting.value,
width: '150px'
}).component();
valueContainer.addItem(valueBox, { CSSStyles: { 'margin-right': '0px' } });
this.disposables.push(
valueBox.onTextChanged(() => {
if ((this.handleOnTextChanged(valueBox, engineSetting.value))) {
this.parameterUpdates!.set(engineSetting.parameterName!, valueBox.value!);
} else if (this.parameterUpdates!.has(engineSetting.parameterName!)) {
this.parameterUpdates!.delete(engineSetting.parameterName!);
}
})
);
// Information bubble title to show allowed values
let information = this.modelView.modelBuilder.button().withProps({
iconPath: IconPathHelper.information,
width: '15px',
height: '15px',
enabled: false,
title: loc.rangeSetting(engineSetting.min!, engineSetting.max!)
}).component();
valueContainer.addItem(information, { CSSStyles: { 'margin-left': '5px' } });
}
// Can reset individual parameter
const resetParameterButton = this.modelView.modelBuilder.button().withProps({
iconPath: IconPathHelper.reset,
@@ -413,15 +515,16 @@ export abstract class PostgresParametersPage extends DashboardPage {
cancellable: false
},
async (_progress, _token): Promise<void> => {
await this.resetParameter(engineSetting.parameterName!);
const session = await this._postgresModel.controllerModel.acquireAzdataSession();
try {
await this._postgresModel.refresh();
} catch (error) {
vscode.window.showErrorMessage(loc.refreshFailed(error));
this.resetParameter(engineSetting.parameterName!, session);
} finally {
session.dispose();
}
await this._postgresModel.refresh();
}
);
this.removeFromChangedComponents(engineSetting.parameterName!);
vscode.window.showInformationMessage(loc.instanceUpdated(this._postgresModel.info.name));
} catch (error) {
vscode.window.showErrorMessage(loc.instanceUpdateFailed(this._postgresModel.info.name, error));
@@ -429,198 +532,32 @@ export abstract class PostgresParametersPage extends DashboardPage {
})
);
let valueComponent: azdata.Component;
if (engineSetting.type === 'enum') {
// If type is enum, component should be drop down menu
let options = engineSetting.options?.slice(1, -1).split(',');
let values: string[] = [];
options!.forEach(option => {
values.push(option.slice(option.indexOf('"') + 1, -1));
});
let valueBox = this.modelView.modelBuilder.dropDown().withProps({
values: values,
value: engineSetting.value,
width: '150px'
}).component();
valueComponent = valueBox;
this.disposables.push(
valueBox.onValueChanged(() => {
if (engineSetting.value !== String(valueBox.value)) {
this.parameterUpdates.set(engineSetting.parameterName!, String(valueBox.value));
this.collectChangedComponents(engineSetting.parameterName!);
this.saveButton.enabled = true;
this.discardButton.enabled = true;
} else if (this.parameterUpdates.has(engineSetting.parameterName!)) {
this.parameterUpdates.delete(engineSetting.parameterName!);
this.removeFromChangedComponents(engineSetting.parameterName!);
}
})
);
} else if (engineSetting.type === 'bool') {
// If type is bool, component should be checkbox to turn on or off
let valueBox = this.modelView.modelBuilder.checkBox().withProps({
label: loc.on,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
valueComponent = valueBox;
if (engineSetting.value === 'on') {
valueBox.checked = true;
} else {
valueBox.checked = false;
}
this.disposables.push(
valueBox.onChanged(() => {
if (valueBox.checked && engineSetting.value === 'off') {
this.parameterUpdates.set(engineSetting.parameterName!, loc.on);
this.collectChangedComponents(engineSetting.parameterName!);
this.saveButton.enabled = true;
this.discardButton.enabled = true;
} else if (!valueBox.checked && engineSetting.value === 'on') {
this.parameterUpdates.set(engineSetting.parameterName!, loc.off);
this.collectChangedComponents(engineSetting.parameterName!);
this.saveButton.enabled = true;
this.discardButton.enabled = true;
} else if (this.parameterUpdates.has(engineSetting.parameterName!)) {
this.parameterUpdates.delete(engineSetting.parameterName!);
this.removeFromChangedComponents(engineSetting.parameterName!);
}
})
);
} else if (engineSetting.type === 'string') {
// If type is string, component should be text inputbox
let valueBox = this.modelView.modelBuilder.inputBox().withProps({
required: true,
readOnly: false,
value: engineSetting.value,
width: '150px'
}).component();
valueComponent = valueBox;
this.disposables.push(
valueBox.onTextChanged(() => {
if ((this.handleOnTextChanged(valueBox, engineSetting.parameterName!, engineSetting.value))) {
this.parameterUpdates.set(engineSetting.parameterName!, `"${valueBox.value!}"`);
} else if (this.parameterUpdates.has(engineSetting.parameterName!)) {
this.parameterUpdates.delete(engineSetting.parameterName!);
}
})
);
} else {
// If type is real or interger, component should be inputbox set to inputType of number. Max and min values also set.
let valueBox = this.modelView.modelBuilder.inputBox().withProps({
required: true,
readOnly: false,
min: parseInt(engineSetting.min!),
max: parseInt(engineSetting.max!),
inputType: 'number',
value: engineSetting.value,
width: '150px'
}).component();
valueComponent = valueBox;
this.disposables.push(
valueBox.onTextChanged(() => {
if ((this.handleOnTextChanged(valueBox, engineSetting.parameterName!, engineSetting.value))) {
this.parameterUpdates.set(engineSetting.parameterName!, valueBox.value!);
} else if (this.parameterUpdates.has(engineSetting.parameterName!)) {
this.parameterUpdates.delete(engineSetting.parameterName!);
}
})
);
// Information bubble title to show allowed values
let information = this.modelView.modelBuilder.button().withProps({
iconPath: IconPathHelper.information,
width: '15px',
height: '15px',
enabled: false,
title: loc.rangeSetting(engineSetting.min!, engineSetting.max!)
}).component();
return {
parameterName: engineSetting.parameterName!,
originalValue: engineSetting.value!,
valueComponent: valueComponent,
information: information,
description: engineSetting.description!,
resetButton: resetParameterButton
};
}
return {
let parameter: ParametersModel = {
parameterName: engineSetting.parameterName!,
originalValue: engineSetting.value!,
valueComponent: valueComponent,
valueContainer: valueContainer,
description: engineSetting.description!,
resetButton: resetParameterButton
};
return parameter;
}
private collectChangedComponents(name: string): void {
if (!this.changedComponentValues.has(name)) {
this.changedComponentValues.add(name);
}
}
protected abstract saveParameterEdits(engineSettings: string, session: azdataExt.AzdataSession): void;
private removeFromChangedComponents(name: string): void {
if (this.changedComponentValues.has(name)) {
this.changedComponentValues.delete(name);
}
}
protected abstract resetAllParameters(session: azdataExt.AzdataSession): void;
private discardParametersTableChanges(): void {
let instanceOfCheckBox = function (object: any): object is azdata.CheckBoxComponent {
return 'checked' in object;
};
protected abstract resetParameter(parameterName: string, session: azdataExt.AzdataSession): void;
this.changedComponentValues.forEach(v => {
let param = this._parameters.find(p => p.parameterName === v);
if (instanceOfCheckBox(param!.valueComponent)) {
if (param!.originalValue === 'on') {
param!.valueComponent.checked = true;
} else {
param!.valueComponent.checked = false;
}
} else {
param!.valueComponent.value = param!.originalValue;
}
});
}
protected abstract refreshParametersTable(): void;
private refreshParametersTable(): void {
this._parameters = this.engineSettings.map(parameter => this.createParameterComponents(parameter));
this._parametersTable.data = this._parameters.map(p => {
if (p.information) {
// Container to hold input component and information bubble
const valueContainer = this.modelView.modelBuilder.flexContainer().withLayout({ alignItems: 'center' }).component();
valueContainer.addItem(p.valueComponent, { CSSStyles: { 'margin-right': '0px' } });
valueContainer.addItem(p.information, { CSSStyles: { 'margin-left': '5px' } });
return [p.parameterName, valueContainer, p.description, p.resetButton];
} else {
return [p.parameterName, p.valueComponent, p.description, p.resetButton];
}
});
}
private async handleServiceUpdated(): Promise<void> {
protected async handleServiceUpdated(): Promise<void> {
if (this._postgresModel.configLastUpdated && !this._postgresModel.engineSettingsLastUpdated) {
this.connectToServerButton!.enabled = true;
this.parametersTableLoading!.loading = false;
} else if (this._postgresModel.engineSettingsLastUpdated) {
await this.callGetEngineSettings();
this.discardButton.enabled = false;
this.saveButton.enabled = false;
this.discardButton!.enabled = false;
this.saveButton!.enabled = false;
}
}
protected abstract saveParameterEdits(engineSettings: string): Promise<void>;
protected abstract resetAllParameters(): Promise<void>;
protected abstract resetParameter(parameterName: string): Promise<void>;
}

View File

@@ -17,8 +17,8 @@ export class PostgresPropertiesPage extends DashboardPage {
private loading?: azdata.LoadingComponent;
private keyValueContainer?: KeyValueContainer;
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
super(modelView, dashboard);
constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
super(modelView);
this.disposables.push(this._postgresModel.onConfigUpdated(
() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())));

View File

@@ -1,335 +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, iconSize } from '../../../constants';
import { DashboardPage } from '../../components/dashboardPage';
import { PostgresModel } from '../../../models/postgresModel';
export type PodHealthModel = {
condition: string,
details?: azdata.Component,
lastUpdate: string
};
export enum PodConditionType {
initialized = 'Initialized',
ready = 'Ready',
containersReady = 'ContainersReady',
podScheduled = 'PodScheduled'
}
export class PostgresResourceHealthPage extends DashboardPage {
private podSummaryContainer!: azdata.DivContainer;
private podConditionsContainer!: azdata.DivContainer;
private podConditionsLoading!: azdata.LoadingComponent;
private podConditionsTable!: azdata.DeclarativeTableComponent;
private podConditionsTableIndexes: Map<string, number[]> = new Map();
private podDropDown!: azdata.DropDownComponent;
private coordinatorPodName!: string;
private coordinatorData: PodHealthModel[] = [];
private podsData: PodHealthModel[] = [];
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _postgresModel: PostgresModel) {
super(modelView, dashboard);
this.disposables.push(
this._postgresModel.onConfigUpdated(() => this.eventuallyRunOnInitialized(() => this.handleConfigUpdated())));
}
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': '10px 20px 0px 20px' } });
content.addItem(this.modelView.modelBuilder.text().withProps({
value: loc.resourceHealth,
CSSStyles: { ...cssStyles.title }
}).component());
content.addItem(this.modelView.modelBuilder.text().withProps({
value: loc.resourceHealthDescription,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component());
this.podSummaryContainer = this.modelView.modelBuilder.divContainer().component();
this.refreshPodSummarySection();
content.addItem(this.podSummaryContainer);
// Pod Conditions
content.addItem(this.modelView.modelBuilder.text().withProps({
value: loc.podsPresent,
CSSStyles: { ...cssStyles.title }
}).component());
content.addItem(this.modelView.modelBuilder.text().withProps({
value: loc.podsUsedDescription,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px', 'margin-top': '10px' }
}).component());
this.podConditionsContainer = this.modelView.modelBuilder.divContainer().component();
this.podConditionsTable = this.modelView.modelBuilder.declarativeTable().withProps({
width: '100%',
columns: [
{
displayName: loc.condition,
valueType: azdata.DeclarativeDataType.string,
isReadOnly: true,
width: '20%',
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: cssStyles.tableRow
},
{
displayName: loc.details,
valueType: azdata.DeclarativeDataType.component,
isReadOnly: true,
width: '50%',
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: {
...cssStyles.tableRow,
'min-width': '150px'
}
},
{
displayName: loc.lastTransition,
valueType: azdata.DeclarativeDataType.string,
isReadOnly: true,
width: '30%',
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: cssStyles.tableRow
}
],
data: [this.coordinatorData.map(p => [p.condition, p.details, p.lastUpdate])]
}).component();
this.podDropDown = this.modelView.modelBuilder.dropDown().withProps({ width: '150px' }).component();
this.disposables.push(
this.podDropDown.onValueChanged(() => {
this.podConditionsTable.setFilter(this.podConditionsTableIndexes.get(String(this.podDropDown.value)));
})
);
this.podConditionsContainer.addItem(this.podDropDown, { CSSStyles: { 'margin': '10px 0px 10px 0px' } });
this.podConditionsContainer.addItem(this.podConditionsTable);
this.podConditionsLoading = this.modelView.modelBuilder.loadingComponent()
.withItem(this.podConditionsContainer)
.withProperties<azdata.LoadingComponentProperties>({
loading: !this._postgresModel.configLastUpdated
}).component();
this.refreshPodConditions();
content.addItem(this.podConditionsLoading, { CSSStyles: cssStyles.text });
this.initialized = true;
return root;
}
protected get toolbarContainer(): azdata.ToolbarContainer {
// Refresh
const refreshButton = this.modelView.modelBuilder.button().withProps({
label: loc.refresh,
iconPath: IconPathHelper.refresh
}).component();
this.disposables.push(
refreshButton.onDidClick(async () => {
refreshButton.enabled = false;
try {
this.podConditionsLoading!.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 createPodList(): string[] {
const podStatus = this._postgresModel.config?.status.podsStatus;
let podNames: string[] = [];
podStatus?.forEach(p => {
let podHealthModels: PodHealthModel[] = [];
let indexes: number[] = [];
p.conditions.forEach(c => {
let message: string;
let imageComponent = this.modelView.modelBuilder.image().withProps({
width: iconSize,
height: iconSize,
iconHeight: '15px',
iconWidth: '15px'
}).component();
if (c.status === 'False') {
imageComponent.iconPath = IconPathHelper.fail;
message = c.message ?? c.reason ?? '';
} else {
imageComponent.iconPath = IconPathHelper.success;
if (c.type === PodConditionType.initialized) {
message = loc.podInitialized;
} else if (c.type === PodConditionType.ready) {
message = loc.podReady;
} else if (c.type === PodConditionType.containersReady) {
message = loc.containerReady;
} else if (c.type === PodConditionType.podScheduled) {
message = loc.podScheduled;
} else {
message = c.message ?? c.reason ?? '';
}
}
const conditionContainer = this.modelView.modelBuilder.flexContainer().withProps({
CSSStyles: { 'alignItems': 'center', 'height': '15px' }
}).component();
conditionContainer.addItem(imageComponent, { CSSStyles: { 'margin-right': '0px' } });
conditionContainer.addItem(this.modelView.modelBuilder.text().withProps({
value: message,
}).component());
indexes.push(this.podsData.length);
this.podsData.push({
condition: c.type,
details: conditionContainer,
lastUpdate: c.lastTransitionTime
});
});
if (p.role.toUpperCase() !== loc.coordinator.toUpperCase()) {
podNames.push(p.name);
} else {
this.coordinatorData = podHealthModels;
this.coordinatorPodName = p.name;
podNames.unshift(p.name);
}
this.podConditionsTableIndexes.set(p.name, indexes);
});
this.podConditionsTable.data = this.podsData.map(p => [p.condition, p.details, p.lastUpdate]);
return podNames;
}
private findPodIssues(): string[] {
const podStatus = this._postgresModel.config?.status.podsStatus;
let issueCount = 0;
let podIssuesDetected: string[] = [];
podStatus?.forEach(p => {
p.conditions.forEach(c => {
if (c.status === 'False') {
issueCount++;
}
});
if (issueCount > 0) {
podIssuesDetected.push(loc.numberOfIssuesDetected(p.name, issueCount));
issueCount = 0;
}
});
return podIssuesDetected;
}
private refreshPodSummarySection(): void {
let podSummaryTitle = this.modelView.modelBuilder.flexContainer().withProps({
CSSStyles: { 'alignItems': 'center', 'height': '15px', 'margin-top': '20px' }
}).component();
if (!this._postgresModel.config) {
podSummaryTitle.addItem(this.modelView.modelBuilder.loadingComponent().component(), { CSSStyles: { 'margin-right': '5px' } });
podSummaryTitle.addItem(this.modelView.modelBuilder.text().withProps({
value: loc.loading,
CSSStyles: { ...cssStyles.title }
}).component());
this.podSummaryContainer.addItem(podSummaryTitle);
} else {
let components: azdata.Component[] = [];
let imageComponent = this.modelView.modelBuilder.image().withProps({
iconPath: IconPathHelper.success,
width: iconSize,
height: iconSize,
iconHeight: '20px',
iconWidth: '20px'
}).component();
let podIssues = this.findPodIssues();
if (podIssues.length === 0) {
imageComponent.iconPath = IconPathHelper.success;
podSummaryTitle.addItem(imageComponent, { CSSStyles: { 'margin-right': '5px' } });
podSummaryTitle.addItem(this.modelView.modelBuilder.text().withProps({
value: loc.available,
CSSStyles: { ...cssStyles.title, 'margin-left': '0px' }
}).component());
components.push(podSummaryTitle);
components.push(this.modelView.modelBuilder.text().withProps({
value: loc.noPodIssuesDetected,
CSSStyles: { ...cssStyles.text, 'margin-top': '20px' }
}).component());
} else {
imageComponent.iconPath = IconPathHelper.fail;
podSummaryTitle.addItem(imageComponent, { CSSStyles: { 'margin-right': '5px' } });
podSummaryTitle.addItem(this.modelView.modelBuilder.text().withProps({
value: loc.issuesDetected,
CSSStyles: { ...cssStyles.title }
}).component());
components.push(podSummaryTitle);
components.push(this.modelView.modelBuilder.text().withProps({
value: loc.podIssuesDetected,
CSSStyles: { ...cssStyles.text, 'margin-top': '20px 0px 10px 0px' }
}).component());
components.push(...podIssues.map(i => {
return this.modelView.modelBuilder.text().withProps({
value: i,
CSSStyles: { ...cssStyles.text, 'margin': '0px' }
}).component();
}));
}
this.podSummaryContainer.addItems(components);
}
}
private refreshPodConditions(): void {
if (this._postgresModel.config) {
this.podConditionsTableIndexes = new Map();
this.podsData = [];
this.podDropDown.values = this.createPodList();
this.podConditionsTable.setFilter(this.podConditionsTableIndexes.get(this.coordinatorPodName!));
this.podConditionsLoading.loading = false;
}
}
private handleConfigUpdated() {
this.podSummaryContainer.clearItems();
this.refreshPodSummarySection();
this.refreshPodConditions();
}
}

View File

@@ -13,8 +13,8 @@ import { ResourceType } from 'arc';
import { PostgresModel } from '../../../models/postgresModel';
export class PostgresSupportRequestPage extends DashboardPage {
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
super(modelView, dashboard);
constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
super(modelView);
}
protected get title(): string {
@@ -44,11 +44,6 @@ export class PostgresSupportRequestPage extends DashboardPage {
CSSStyles: { ...cssStyles.text, 'margin-bottom': '20px' }
}).component());
content.addItem(this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: loc.supportRequestNote,
CSSStyles: { ...cssStyles.text, 'margin-bottom': '20px' }
}).component());
const supportRequestButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
iconPath: IconPathHelper.support,
label: loc.newSupportRequest,

View File

@@ -4,15 +4,16 @@
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as azdataExt from 'azdata-ext';
import * as loc from '../../../localizedConstants';
import { IconPathHelper } from '../../../constants';
import { PostgresParametersPage } from './postgresParameters';
import { PostgresModel, EngineSettingsModel } from '../../../models/postgresModel';
import { PostgresModel } from '../../../models/postgresModel';
export class PostgresWorkerNodeParametersPage extends PostgresParametersPage {
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, postgresModel: PostgresModel) {
super(modelView, dashboard, postgresModel);
constructor(protected modelView: azdata.ModelView, _postgresModel: PostgresModel) {
super(modelView, _postgresModel);
}
protected get title(): string {
@@ -34,32 +35,38 @@ export class PostgresWorkerNodeParametersPage extends PostgresParametersPage {
return loc.nodeParametersDescription;
}
protected get engineSettings(): EngineSettingsModel[] {
return this._postgresModel.workerNodesEngineSettings;
}
protected async saveParameterEdits(engineSettings: string): Promise<void> {
protected async saveParameterEdits(engineSettings: string, session: azdataExt.AzdataSession): Promise<void> {
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name,
{ engineSettings: engineSettings },
this._postgresModel.engineVersion,
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
this._postgresModel.controllerModel.controllerContext);
session);
}
protected async resetAllParameters(): Promise<void> {
protected async resetAllParameters(session: azdataExt.AzdataSession): Promise<void> {
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name,
{ engineSettings: `''`, replaceEngineSettings: true },
this._postgresModel.engineVersion,
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
this._postgresModel.controllerModel.controllerContext);
session);
}
protected async resetParameter(parameterName: string): Promise<void> {
protected async resetParameter(parameterName: string, session: azdataExt.AzdataSession): Promise<void> {
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name,
{ engineSettings: parameterName + '=' },
this._postgresModel.engineVersion,
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
this._postgresModel.controllerModel.controllerContext);
session);
}
protected refreshParametersTable(): void {
this._parameters = this._postgresModel.workerNodesEngineSettings.map(engineSetting => this.createParameterComponents(engineSetting));
this._parametersTable.data = this._parameters.map(p => [p.parameterName, p.valueContainer, p.description, p.resetButton]);
}
}

View File

@@ -15,7 +15,7 @@ import { InitializingComponent } from '../components/initializingComponent';
import { AzureArcTreeDataProvider } from '../tree/azureArcTreeDataProvider';
import { getErrorMessage } from '../../common/utils';
import { RadioOptionsGroup } from '../components/radioOptionsGroup';
import { getCurrentClusterContext, getDefaultKubeConfigPath, getKubeConfigClusterContexts, KubeClusterContext } from '../../common/kubeUtils';
import { getCurrentClusterContext, getDefaultKubeConfigPath, getKubeConfigClusterContexts } from '../../common/kubeUtils';
import { FilePicker } from '../components/filePicker';
export type ConnectToControllerDialogModel = { controllerModel: ControllerModel, password: string };
@@ -25,34 +25,24 @@ abstract class ControllerDialogBase extends InitializingComponent {
protected modelBuilder!: azdata.ModelBuilder;
protected dialog: azdata.window.Dialog;
protected namespaceInputBox!: azdata.InputBoxComponent;
protected urlInputBox!: azdata.InputBoxComponent;
protected kubeConfigInputBox!: FilePicker;
protected clusterContextRadioGroup!: RadioOptionsGroup;
protected nameInputBox!: azdata.InputBoxComponent;
protected usernameInputBox!: azdata.InputBoxComponent;
protected passwordInputBox!: azdata.InputBoxComponent;
protected urlInputBox!: azdata.InputBoxComponent;
private _kubeClusters: KubeClusterContext[] = [];
protected dispose(): void {
this._toDispose.forEach(disposable => disposable.dispose());
this._toDispose.length = 0;
this._toDispose.length = 0; // clear the _toDispose array
}
protected getComponents(): (azdata.FormComponent<azdata.Component> & { layout?: azdata.FormItemLayout | undefined; })[] {
return [
{
component: this.namespaceInputBox,
title: loc.namespace,
required: true
},
{
component: this.urlInputBox,
title: loc.controllerUrl,
layout: {
info: loc.controllerUrlDescription
}
required: true
}, {
component: this.kubeConfigInputBox.component(),
title: loc.controllerKubeConfig,
@@ -64,17 +54,14 @@ abstract class ControllerDialogBase extends InitializingComponent {
}, {
component: this.nameInputBox,
title: loc.controllerName,
required: false,
layout: {
info: loc.controllerNameDescription
}
required: false
}, {
component: this.usernameInputBox,
title: loc.controllerUsername,
title: loc.username,
required: true
}, {
component: this.passwordInputBox,
title: loc.controllerPassword,
title: loc.password,
required: true
}
];
@@ -84,14 +71,11 @@ abstract class ControllerDialogBase extends InitializingComponent {
protected readonlyFields(): azdata.Component[] { return []; }
protected initializeFields(controllerInfo: ControllerInfo | undefined, password: string | undefined) {
this.namespaceInputBox = this.modelBuilder.inputBox()
.withProps({
value: controllerInfo?.namespace,
}).component();
this.urlInputBox = this.modelBuilder.inputBox()
.withProps({
value: controllerInfo?.endpoint,
placeHolder: loc.controllerUrlPlaceholder,
.withProperties<azdata.InputBoxProperties>({
value: controllerInfo?.url,
// If we have a model then we're editing an existing connection so don't let them modify the URL
readOnly: !!controllerInfo
}).component();
this.kubeConfigInputBox = new FilePicker(
this.modelBuilder,
@@ -99,23 +83,22 @@ abstract class ControllerDialogBase extends InitializingComponent {
(disposable) => this._toDispose.push(disposable)
);
this.modelBuilder.inputBox()
.withProps({
.withProperties<azdata.InputBoxProperties>({
value: controllerInfo?.kubeConfigFilePath || getDefaultKubeConfigPath()
}).component();
this.clusterContextRadioGroup = new RadioOptionsGroup(this.modelBuilder, (disposable) => this._toDispose.push(disposable));
this.loadRadioGroup(controllerInfo?.kubeClusterContext);
this._toDispose.push(this.clusterContextRadioGroup.onRadioOptionChanged(newContext => this.updateNamespace(newContext)));
this._toDispose.push(this.kubeConfigInputBox.onTextChanged(() => this.loadRadioGroup(controllerInfo?.kubeClusterContext)));
this.nameInputBox = this.modelBuilder.inputBox()
.withProps({
.withProperties<azdata.InputBoxProperties>({
value: controllerInfo?.name
}).component();
this.usernameInputBox = this.modelBuilder.inputBox()
.withProps({
.withProperties<azdata.InputBoxProperties>({
value: controllerInfo?.username
}).component();
this.passwordInputBox = this.modelBuilder.inputBox()
.withProps({
.withProperties<azdata.InputBoxProperties>({
inputType: 'password',
value: password
}).component();
@@ -131,22 +114,15 @@ abstract class ControllerDialogBase extends InitializingComponent {
}
private loadRadioGroup(previousClusterContext?: string): void {
this.clusterContextRadioGroup.load(() => {
this._kubeClusters = getKubeConfigClusterContexts(this.kubeConfigInputBox.value!);
const currentClusterContext = getCurrentClusterContext(this._kubeClusters, previousClusterContext, false);
this.namespaceInputBox.value = currentClusterContext.namespace || this.namespaceInputBox.value;
this.clusterContextRadioGroup.load(async () => {
const clusters = await getKubeConfigClusterContexts(this.kubeConfigInputBox.value!);
return {
values: this._kubeClusters.map(c => c.name),
defaultValue: currentClusterContext.name
values: clusters.map(c => c.name),
defaultValue: getCurrentClusterContext(clusters, previousClusterContext, false),
};
});
}
private updateNamespace(currentContextName: string | undefined): void {
const currentContext = this._kubeClusters.find(cluster => cluster.name === currentContextName);
this.namespaceInputBox.value = currentContext?.namespace;
}
public showDialog(controllerInfo?: ControllerInfo, password: string | undefined = undefined): azdata.window.Dialog {
this.id = controllerInfo?.id ?? uuid();
this.resources = controllerInfo?.resources ?? [];
@@ -192,8 +168,7 @@ abstract class ControllerDialogBase extends InitializingComponent {
protected getControllerInfo(url: string, rememberPassword: boolean = false): ControllerInfo {
return {
id: this.id,
endpoint: url || undefined,
namespace: this.namespaceInputBox.value!,
url: url,
kubeConfigFilePath: this.kubeConfigInputBox.value!,
kubeClusterContext: this.clusterContextRadioGroup.value!,
name: this.nameInputBox.value ?? '',
@@ -208,7 +183,7 @@ export class ConnectToControllerDialog extends ControllerDialogBase {
protected rememberPwCheckBox!: azdata.CheckBoxComponent;
protected fieldToFocusOn() {
return this.namespaceInputBox;
return this.urlInputBox;
}
protected getComponents() {
@@ -234,25 +209,22 @@ export class ConnectToControllerDialog extends ControllerDialogBase {
}
public async validate(): Promise<boolean> {
if (!this.namespaceInputBox.value || !this.usernameInputBox.value || !this.passwordInputBox.value) {
if (!this.urlInputBox.value || !this.usernameInputBox.value || !this.passwordInputBox.value) {
return false;
}
let url = this.urlInputBox.value || '';
if (url) {
// Only support https connections
if (url.toLowerCase().startsWith('http://')) {
url = url.replace('http', 'https');
}
// Append https if they didn't type it in
if (!url.toLowerCase().startsWith('https://')) {
url = `https://${url}`;
}
// Append default port if one wasn't specified
if (!/.*:\d*$/.test(url)) {
url = `${url}:30080`;
}
let url = this.urlInputBox.value;
// Only support https connections
if (url.toLowerCase().startsWith('http://')) {
url = url.replace('http', 'https');
}
// Append https if they didn't type it in
if (!url.toLowerCase().startsWith('https://')) {
url = `https://${url}`;
}
// Append default port if one wasn't specified
if (!/.*:\d*$/.test(url)) {
url = `${url}:30080`;
}
const controllerInfo: ControllerInfo = this.getControllerInfo(url, !!this.rememberPwCheckBox.checked);
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
try {
@@ -262,7 +234,7 @@ export class ConnectToControllerDialog extends ControllerDialogBase {
controllerModel.info.name = controllerModel.info.name || controllerModel.controllerConfig?.metadata.name || loc.defaultControllerName;
} catch (err) {
this.dialog.message = {
text: loc.connectToControllerFailed(this.namespaceInputBox.value, err),
text: loc.connectToControllerFailed(this.urlInputBox.value, err),
level: azdata.window.MessageLevel.Error
};
return false;
@@ -295,16 +267,11 @@ export class PasswordToControllerDialog extends ControllerDialogBase {
if (!this.passwordInputBox.value) {
return false;
}
const controllerInfo: ControllerInfo = this.getControllerInfo(this.urlInputBox.value!, false);
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
const azdataApi = <azdataExt.IExtension>vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
try {
await azdataApi.azdata.login(
{
endpoint: controllerInfo.endpoint,
namespace: controllerInfo.namespace
},
controllerInfo.username,
this.urlInputBox.value!,
this.usernameInputBox.value!,
this.passwordInputBox.value,
{
'KUBECONFIG': this.kubeConfigInputBox.value!,
@@ -326,6 +293,8 @@ export class PasswordToControllerDialog extends ControllerDialogBase {
return false;
}
}
const controllerInfo: ControllerInfo = this.getControllerInfo(this.urlInputBox.value!, false);
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
this.completionPromise.resolve({ controllerModel: controllerModel, password: this.passwordInputBox.value });
return true;
}

View File

@@ -10,7 +10,7 @@ import { ControllerModel } from '../../models/controllerModel';
import { ControllerTreeNode } from './controllerTreeNode';
import { TreeNode } from './treeNode';
const mementoToken = 'arcDataControllers.v2';
const mementoToken = 'arcDataControllers';
/**
* The TreeDataProvider for the Azure Arc view, which displays a list of registered

View File

@@ -44,7 +44,7 @@ export class ControllerTreeNode extends TreeNode {
} catch (err) {
vscode.window.showErrorMessage(loc.errorConnectingToController(err));
try {
await this.model.refresh(false);
await this.model.refresh(false, true);
this.updateChildren(this.model.registrations);
} catch (err) {
if (!(err instanceof UserCancelledError)) {

View File

@@ -182,10 +182,10 @@
resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd"
integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==
"@microsoft/azdata-test@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@microsoft/azdata-test/-/azdata-test-1.5.0.tgz#5ffa9ec6b704fea439c63d7dfa46dcfcf3236747"
integrity sha512-kaDn5geXqrhcZgxCWXSrbXdUpJi5TFmi+sIPDfmhMYJa8uecn9C2rzxn5ZbxBN5cjjYOWF318dERfe+S0CWnlA==
"@microsoft/azdata-test@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@microsoft/azdata-test/-/azdata-test-1.4.0.tgz#a809187ae8a065c518e3a3e2d350883e592853bc"
integrity sha512-iscDA13/XRknRCNauP9OPsSg/ulTrMJOPFA0XMyNG1it3zY8mEJxxFJcNkWTnnEWpOUFvyksvoouzYUNy1fvrQ==
dependencies:
http-proxy-agent "^2.1.0"
https-proxy-agent "^2.2.4"

View File

@@ -1,486 +0,0 @@
{
"metadata": {
"kernelspec": {
"name": "python3",
"display_name": "Python 3",
"language": "python"
},
"language_info": {
"name": "python",
"version": "3.6.6",
"mimetype": "text/x-python",
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"pygments_lexer": "ipython3",
"nbconvert_exporter": "python",
"file_extension": ".py"
}
},
"nbformat_minor": 2,
"nbformat": 4,
"cells": [
{
"cell_type": "markdown",
"source": [
"![Microsoft](https://raw.githubusercontent.com/microsoft/azuredatastudio/main/extensions/resource-deployment/images/microsoft-small-logo.png)\n",
"## Deploy Azure SQL Edge to an Azure VM via IoT hub\n",
"This notebook will walk you through the end to end setup of Azure SQL Edge.\n",
"1. Create an Azure Edge VM as a virtual IoT device, you can go the \"Default Settings\" cell and adjust the vm_size variable based on your needs. Available sizes and pricing information can be found [here](https://docs.microsoft.com/azure/virtual-machines/linux/sizes).\n",
"1. Create an Azure IoT hub, you can go to the \"Default Settings\" cell and adjust value of the following variables based on your needs: iot_hub_sku and iot_hub_units. Available SKUs and pricing information can be found [here](https://azure.microsoft.com/pricing/details/iot-hub/).\n",
"1. Add the device to the IoT hub\n",
"1. Deploy SQL Edge module to the device with optional package file\n",
"1. Enable connecting to the SQL Edge instance on the device\n",
"\n",
"### Dependencies\n",
"- Azure CLI. For more information, see [Azure CLI Installation](https://docs.microsoft.com/cli/azure/install-azure-cli?view=azure-cli-latest).\n",
"\n",
"<span style=\"color:red\"><font size=\"3\">Please press the \"Run all\" button to run the notebook</font></span>"
],
"metadata": {
"azdata_cell_guid": "15b8cfc7-dd7f-4db8-9a3c-2151932fe7b5"
}
},
{
"cell_type": "markdown",
"source": [
"### Check dependencies"
],
"metadata": {
"azdata_cell_guid": "f78f4ff3-d4c9-4c3e-853f-4add05061eb0"
}
},
{
"cell_type": "code",
"source": [
"import sys,os,json,html,getpass,time,ntpath,uuid\n",
"\n",
"def run_command(command:str, displayCommand:str = \"\", returnObject:bool = False):\n",
" print(\"Executing: \" + (displayCommand if displayCommand != \"\" else command))\n",
" if returnObject:\n",
" output = os.popen(command).read()\n",
" print(f'Command successfully executed')\n",
" return json.loads(''.join(output))\n",
" else:\n",
" !{command}\n",
" if _exit_code != 0:\n",
" sys.exit(f'Command execution failed with exit code: {str(_exit_code)}.\\n')\n",
" else:\n",
" print(f'Command successfully executed')\n",
"\n",
"run_command(command='az --version')"
],
"metadata": {
"azdata_cell_guid": "70b9744f-eb59-44e8-9b35-db590ac4651d",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Install Azure IoT extension for Azure CLI"
],
"metadata": {
"azdata_cell_guid": "a7f15c68-1725-4caa-b4f7-ddc2b4934883"
}
},
{
"cell_type": "code",
"source": [
"extensions = run_command('az extension list', returnObject=True)\r\n",
"extensions = [ext for ext in extensions if ext['name'] == 'azure-cli-iot-ext']\r\n",
"if len(extensions) > 0:\r\n",
" run_command('az extension remove --name azure-cli-iot-ext')\r\n",
"run_command('az extension add --name azure-iot')"
],
"metadata": {
"azdata_cell_guid": "55bb2f96-6f7f-4aa0-9daf-d0f7f9d9243c",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Required information"
],
"metadata": {
"azdata_cell_guid": "b5dc5586-06e8-44d9-8bc3-2861d510efe5"
}
},
{
"cell_type": "code",
"source": [
"azure_subscription_id = os.environ[\"AZDATA_NB_VAR_ASDE_SUBSCRIPTIONID\"]\n",
"azure_resource_group = os.environ[\"AZDATA_NB_VAR_ASDE_RESOURCEGROUP\"]\n",
"azure_location = os.environ[\"AZDATA_NB_VAR_ASDE_AZURE_LOCATION\"]\n",
"sa_password = os.environ[\"AZDATA_NB_VAR_SA_PASSWORD\"]\n",
"vm_admin = os.environ[\"AZDATA_NB_VAR_ASDE_VM_ADMIN\"]\n",
"vm_password = os.environ[\"AZDATA_NB_VAR_ASDE_VM_PASSWORD\"]\n",
"package_path = os.environ[\"AZDATA_NB_VAR_ASDE_PACKAGE_PATH\"]\n",
"sql_port = os.environ[\"AZDATA_NB_VAR_ASDE_SQL_PORT\"]\n",
"new_rg_flag = os.environ[\"AZDATA_NB_VAR_ASDE_NEW_RESOURCEGROUP\"]\n",
"new_rg_name = os.environ[\"AZDATA_NB_VAR_ASDE_NEW_RESOURCEGROUP_NAME\"]\n",
"\n",
"if new_rg_flag == 'true':\n",
" azure_resource_group = new_rg_name\n",
"print(f'Subscription: {azure_subscription_id}')\n",
"print(f'Resource group: {azure_resource_group}')\n",
"print(f'Location: {azure_location}')\n",
"print(f'VM admin username: {vm_admin}')\n",
"print(f'VM admin password: ******')\n",
"print(f'SQL Server port: {sql_port}')\n",
"print(f'SQL Server sa password: ******')\n",
"print(f'Package path: {package_path}')"
],
"metadata": {
"azdata_cell_guid": "dde9388b-f623-4d62-bb74-36a05f5d2ea3",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Default settings"
],
"metadata": {
"azdata_cell_guid": "2a5755eb-85a7-4237-8d87-04cdab13cf40"
}
},
{
"cell_type": "code",
"source": [
"suffix = time.strftime(\"%y%m%d%H%M%S\", time.localtime())\n",
"iot_hub_name = f'hub{suffix}'\n",
"iot_hub_sku = 'S1'\n",
"iot_hub_units = 4\n",
"iot_device_id = f'vm{suffix}'\n",
"azure_storage_account = f'sa{suffix}'\n",
"storage_account_container = 'sqldatabasepackage'\n",
"sql_lcid = '1033'\n",
"sql_collation = 'SQL_Latin1_General_CP1_CI_AS'\n",
"vm_size = 'Standard_DS1_v2'"
],
"metadata": {
"azdata_cell_guid": "19ebeaf4-94c9-4d2b-bd9f-e3c6bf7f2dda",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Login to Azure"
],
"metadata": {
"azdata_cell_guid": "84f57c09-5772-4f7a-a270-4039b8d5b081"
}
},
{
"cell_type": "code",
"source": [
"run_command('az login')"
],
"metadata": {
"azdata_cell_guid": "f9e8ddee-aefa-4951-b767-b318d941d2cd",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Set active Azure subscription"
],
"metadata": {
"azdata_cell_guid": "59249fa6-f76c-4e5d-bee7-a9ebef6f873e"
}
},
{
"cell_type": "code",
"source": [
"if azure_subscription_id != \"\":\n",
" run_command(f'az account set --subscription {azure_subscription_id}')\n",
"else:\n",
" print('Using the default Azure subscription', {azure_subscription_id})\n",
"run_command(f'az account show')"
],
"metadata": {
"azdata_cell_guid": "6e085676-2cc5-4af8-819c-fa210244e6c3",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Create resource group"
],
"metadata": {
"azdata_cell_guid": "67dacbaa-92f4-4d06-90bb-8974964852aa"
}
},
{
"cell_type": "code",
"source": [
"rg_exists = run_command(f'az group exists --name {azure_resource_group}', returnObject=True)\n",
"\n",
"if rg_exists:\n",
" print(f'resource group \\\"{azure_resource_group}\\\" already exists.')\n",
"else:\n",
" run_command(f'az group create --location {azure_location} --name {azure_resource_group}')"
],
"metadata": {
"azdata_cell_guid": "f29b439e-cf05-4c35-aa47-1482ccd653bf",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Create IoT hub"
],
"metadata": {
"azdata_cell_guid": "e37a04c3-515d-4cb7-99b2-f8bc6167510e"
}
},
{
"cell_type": "code",
"source": [
"hub_list = run_command(f'az iot hub list --resource-group {azure_resource_group}', returnObject=True)\n",
"hub_list = [hub for hub in hub_list if hub['name'] == iot_hub_name]\n",
"if len(hub_list) == 0:\n",
" run_command(f'az iot hub create --name {iot_hub_name} --resource-group {azure_resource_group} --location {azure_location} --sku {iot_hub_sku} --unit {iot_hub_units}')\n",
"else:\n",
" print(f'IoT hub \\\"{iot_hub_name}\\\" already exists')"
],
"metadata": {
"azdata_cell_guid": "f9f5e4ec-82a5-45df-a408-ddb0fb21847c",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Create storage account and storage account container, then upload the package"
],
"metadata": {
"azdata_cell_guid": "90ec2b26-0c4a-4aa4-b397-f16b09b454ea"
}
},
{
"cell_type": "code",
"source": [
"storage_account_created = False\n",
"if package_path == \"\":\n",
" print(f'Package file not provided')\n",
" blob_sas = ''\n",
"else: \n",
" package_name = ntpath.basename(package_path)\n",
" storage_accounts = run_command(f'az storage account list --resource-group {azure_resource_group} --subscription {azure_subscription_id}', returnObject=True)\n",
" storage_accounts = [storage_account for storage_account in storage_accounts if storage_account['name'] == azure_storage_account]\n",
" if len(storage_accounts) == 0:\n",
" storage_account_created = True\n",
" run_command(f'az storage account create -n {azure_storage_account} -g {azure_resource_group} -l {azure_location} --sku Standard_LRS --kind Storage')\n",
" else:\n",
" print(f'storage account \\\"{azure_storage_account}\\\" already exists.')\n",
"\n",
" storage_account_key = run_command(f'az storage account keys list --account-name {azure_storage_account} --resource-group {azure_resource_group}', returnObject=True)[0]['value']\n",
" container_exists = run_command(f'az storage container exists --name {storage_account_container} --account-key {storage_account_key} --account-name {azure_storage_account} --auth-mode key --output json', returnObject=True)['exists']\n",
" if container_exists:\n",
" print(f'storage account container \\\"{storage_account_container}\\\" already exists.')\n",
" else:\n",
" run_command(f'az storage container create --name {storage_account_container} --account-key {storage_account_key} --account-name {azure_storage_account} --auth-mode key')\n",
"\n",
" blob_exists = run_command(f'az storage blob exists --container-name {storage_account_container} --name \\\"{package_name}\\\" --account-key {storage_account_key} --account-name {azure_storage_account} --auth-mode key', returnObject=True)['exists']\n",
" if blob_exists:\n",
" print(f'blob \\\"{package_name}\\\" already exists.')\n",
" else:\n",
" run_command(f'az storage blob upload --account-name {azure_storage_account} --container-name {storage_account_container} --name {package_name} --file \\\"{package_path}\\\" --account-key {storage_account_key} --auth-mode key')\n",
" now = time.localtime()\n",
" expiry = f'{(now.tm_year + 1)}-{now.tm_mon}-{now.tm_mday}'\n",
" blob_sas = run_command(f'az storage blob generate-sas --container-name {storage_account_container} --name \\\"{package_name}\\\" --account-name {azure_storage_account} --account-key {storage_account_key} --auth-mode key --full-uri --https-only --permissions r --expiry {expiry}', returnObject=True)"
],
"metadata": {
"azdata_cell_guid": "7ab2b3ec-0832-40b3-98c0-4aa87320e7ce",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Add the Edge device to the IoT hub"
],
"metadata": {
"azdata_cell_guid": "fbc5f4ac-dfe0-4543-ace1-49b796251910"
}
},
{
"cell_type": "code",
"source": [
"device_list = run_command(f'az iot hub device-identity list --edge-enabled true --hub-name {iot_hub_name} --resource-group {azure_resource_group}', returnObject=True)\n",
"device_list = [device for device in device_list if device['deviceId'] == iot_device_id]\n",
"if len(device_list) == 0:\n",
" run_command(f'az iot hub device-identity create --device-id {iot_device_id} --hub-name {iot_hub_name} --resource-group {azure_resource_group} --edge-enabled true')\n",
"else:\n",
" print(f'Edge device \\\"{iot_device_id}\\\" already exists.')\n",
"connection_string = run_command(f'az iot hub device-identity show-connection-string --device-id {iot_device_id} --hub-name {iot_hub_name} --resource-group {azure_resource_group}', returnObject=True)\n",
"connection_string = connection_string['connectionString']"
],
"metadata": {
"azdata_cell_guid": "c183c3e3-8699-4f29-993b-07bf848336e3",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Create an Edge enabled VM as an Edge device"
],
"metadata": {
"azdata_cell_guid": "41b10249-cd40-4053-b1b0-b02f562789f7"
}
},
{
"cell_type": "code",
"source": [
"iot_deploy_result = run_command((f\"az deployment group create \"\r\n",
"f\"--resource-group {azure_resource_group} \"\r\n",
"f\"--template-uri \\\"https://aka.ms/iotedge-vm-deploy\\\" \"\r\n",
"f\"--parameters vmSize={vm_size} \"\r\n",
"f\"--parameters dnsLabelPrefix={iot_device_id} \"\r\n",
"f\"--parameters adminUsername={vm_admin} \"\r\n",
"f\"--parameters deviceConnectionString={connection_string} \"\r\n",
"f\"--parameters authenticationType=sshPublicKey \"\r\n",
"f\"--parameters adminPasswordOrKey=\\\"{vm_password}\\\"\"), returnObject=True)\r\n",
"vm_resource = [resource for resource in iot_deploy_result['properties']['dependencies'] if resource['resourceType'] == 'Microsoft.Compute/virtualMachines']\r\n",
"if len(vm_resource) != 1:\r\n",
" sys.exit('Failed to deploy the IoT Edge VM')\r\n",
"vm_name = vm_resource[0]['resourceName']\r\n",
"nsg_name = vm_name.replace('vm-','nsg-')\r\n",
"ip_address = run_command(f'az vm show -d -g {azure_resource_group} -n {vm_name} --query publicIps', returnObject=True)\r\n",
"run_command(f'az network nsg rule create --name \\\"SQL\\\" --nsg-name {nsg_name} --priority 100 --resource-group {azure_resource_group} --access Allow --description \\\"Allow SQL\\\" --destination-address-prefixes \\\"*\\\" --destination-port-ranges {sql_port} --direction Inbound --source-address-prefixes Internet --protocol Tcp')"
],
"metadata": {
"azdata_cell_guid": "c8590c65-b274-460d-9659-97e81d2fd3ea",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Deploy Azure SQL Edge to the device"
],
"metadata": {
"azdata_cell_guid": "ec46957f-0795-4c75-804d-f8a7ecb26382"
}
},
{
"cell_type": "code",
"source": [
"manifest = '{\\\"modulesContent\\\":{\\\"$edgeAgent\\\":{\\\"properties.desired\\\":{\\\"modules\\\":{\\\"AzureSQLEdge\\\":{\\\"settings\\\":{\\\"image\\\":\\\"mcr.microsoft.com/azure-sql-edge\\\",\\\"createOptions\\\":\\\"{\\\\\\\"HostConfig\\\\\\\":{\\\\\\\"CapAdd\\\\\\\":[\\\\\\\"SYS_PTRACE\\\\\\\"],\\\\\\\"Binds\\\\\\\":[\\\\\\\"sqlvolume:/sqlvolume\\\\\\\"],\\\\\\\"PortBindings\\\\\\\":{\\\\\\\"1433/tcp\\\\\\\":[{\\\\\\\"HostPort\\\\\\\":\\\\\\\"<SQL_Port>\\\\\\\"}]},\\\\\\\"Mounts\\\\\\\":[{\\\\\\\"Type\\\\\\\":\\\\\\\"volume\\\\\\\",\\\\\\\"Source\\\\\\\":\\\\\\\"sqlvolume\\\\\\\",\\\\\\\"Target\\\\\\\":\\\\\\\"/var/opt/mssql\\\\\\\"}]},\\\\\\\"User\\\\\\\":\\\\\\\"0:0\\\\\\\",\\\\\\\"Env\\\\\\\":[\\\\\\\"MSSQL_AGENT_ENABLED=TRUE\\\\\\\",\\\\\\\"ClientTransportType=AMQP_TCP_Only\\\\\\\",\\\\\\\"PlanId=asde-developer-on-iot-edge\\\\\\\"]}\\\"},\\\"type\\\":\\\"docker\\\",\\\"version\\\":\\\"1.0\\\",\\\"env\\\":{\\\"ACCEPT_EULA\\\":{\\\"value\\\":\\\"Y\\\"},\\\"SA_PASSWORD\\\":{\\\"value\\\":\\\"<Default_SQL_SA_Password>\\\"},\\\"MSSQL_LCID\\\":{\\\"value\\\":\\\"<SQL_LCID>\\\"},\\\"MSSQL_COLLATION\\\":{\\\"value\\\":\\\"<SQL_Collation>\\\"}<PACKAGE_INFO>},\\\"status\\\":\\\"running\\\",\\\"restartPolicy\\\":\\\"always\\\"}},\\\"runtime\\\":{\\\"settings\\\":{\\\"minDockerVersion\\\":\\\"v1.25\\\"},\\\"type\\\":\\\"docker\\\"},\\\"schemaVersion\\\":\\\"1.0\\\",\\\"systemModules\\\":{\\\"edgeAgent\\\":{\\\"settings\\\":{\\\"image\\\":\\\"mcr.microsoft.com/azureiotedge-agent:1.0\\\",\\\"createOptions\\\":\\\"\\\"},\\\"type\\\":\\\"docker\\\"},\\\"edgeHub\\\":{\\\"settings\\\":{\\\"image\\\":\\\"mcr.microsoft.com/azureiotedge-hub:1.0\\\",\\\"createOptions\\\":\\\"{\\\\\\\"HostConfig\\\\\\\":{\\\\\\\"PortBindings\\\\\\\":{\\\\\\\"443/tcp\\\\\\\":[{\\\\\\\"HostPort\\\\\\\":\\\\\\\"443\\\\\\\"}],\\\\\\\"5671/tcp\\\\\\\":[{\\\\\\\"HostPort\\\\\\\":\\\\\\\"5671\\\\\\\"}],\\\\\\\"8883/tcp\\\\\\\":[{\\\\\\\"HostPort\\\\\\\":\\\\\\\"8883\\\\\\\"}]}}}\\\"},\\\"type\\\":\\\"docker\\\",\\\"status\\\":\\\"running\\\",\\\"restartPolicy\\\":\\\"always\\\"}}}},\\\"$edgeHub\\\":{\\\"properties.desired\\\":{\\\"routes\\\":{},\\\"schemaVersion\\\":\\\"1.0\\\",\\\"storeAndForwardConfiguration\\\":{\\\"timeToLiveSecs\\\":7200}}},\\\"AzureSQLEdge\\\":{\\\"properties.desired\\\":{\\\"ASAJobInfo\\\":\\\"<Optional_ASA_Job_SAS_URL>\\\"}}}}'\n",
"package_info = '' if blob_sas == ''else ',\\\"MSSQL_PACKAGE\\\":{\\\"value\\\":\\\"'+blob_sas+'\\\"}'\n",
"manifest = manifest.replace('<PACKAGE_INFO>', package_info).replace('<Default_SQL_SA_Password>',sa_password).replace('<SQL_LCID>',sql_lcid).replace('<SQL_Port>',sql_port).replace('<SQL_Collation>',sql_collation)\n",
"file_name = f'{uuid.uuid4().hex}.json'\n",
"manifest_file = open(file_name, 'w')\n",
"manifest_file.write(manifest)\n",
"manifest_file.close()\n",
"run_command(f'az iot edge set-modules --device-id \\\"{iot_device_id}\\\" --hub-name \\\"{iot_hub_name}\\\" --content \\\"{file_name}\\\" --resource-group {azure_resource_group}')\n",
"os.remove(file_name)"
],
"metadata": {
"azdata_cell_guid": "81a86ff6-5a83-48be-8be7-654d152eea89",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### **Connect to Azure SQL Edge instance in Azure Data Studio**\n",
"Click the link below to connect to the Azure SQL Edge instance, it might take a couple minutes for the service to start."
],
"metadata": {
"azdata_cell_guid": "3bdfa537-a749-45c4-b219-57d296c22739"
}
},
{
"cell_type": "code",
"source": [
"from IPython.display import *\n",
"connectionParameter = '{\"serverName\":\"' + f'{ip_address},{sql_port}' + '\",\"providerName\":\"MSSQL\",\"authenticationType\":\"SqlLogin\",\"userName\": \"sa\",\"password\":' + json.dumps(sa_password) + '}'\n",
"display(HTML('<br/><a href=\"command:azdata.connect?' + html.escape(connectionParameter)+'\"><font size=\"3\">Click here to connect to the Azure SQL Edge instance</font></a><br/>'))\n",
"display(HTML('<br/><span style=\"color:red\"><font size=\"2\">NOTE: The Azure SQL Edge instance password is included in this link, you may want to clear the results of this code cell before saving the notebook.</font></span>'))"
],
"metadata": {
"azdata_cell_guid": "8bc29cce-96a7-4a78-89af-5c73a6431c24",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "code",
"source": [
"if storage_account_created:\r\n",
" delete_storage_account_command = \"run_command(f'az storage account delete -n {azure_storage_account} -g {azure_resource_group} --yes')\"\r\n",
" display(HTML('<span style=\"color:red\"><font size=\"2\">NOTE: A storage account was created to host the package file, you can delete it after the database is created and populated successfully. To delete the storage account, copy the following code to a new code cell and run the cell.</font></span>'))\r\n",
" display(HTML('<span><font size=\"2\">'+delete_storage_account_command+'</font></span>'))"
],
"metadata": {
"azdata_cell_guid": "8b74ac43-a871-4d28-832d-e6da586f6d3a",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
}
]
}

View File

@@ -2,8 +2,7 @@
"metadata": {
"kernelspec": {
"name": "python3",
"display_name": "Python 3",
"language": "python"
"display_name": "Python 3"
},
"language_info": {
"name": "python",
@@ -163,15 +162,21 @@
"cell_type": "code",
"source": [
"suffix = time.strftime(\"%y%m%d%H%M%S\", time.localtime())\n",
"network_security_group = f'nsg{suffix}'\n",
"public_ip_address_name = f'ip{suffix}'\n",
"iot_hub_name = f'hub{suffix}'\n",
"iot_hub_sku = 'S1'\n",
"iot_hub_units = 4\n",
"iot_device_id = f'vm{suffix}'\n",
"vm_size = 'Standard_DS3_v2'\n",
"vnet_name = f'net{suffix}'\n",
"subnet_name = f'subnet{suffix}'\n",
"subnet_address_prefix = '10.0.0.0/24'\n",
"vnet_address_prefix = '10.0.0.0/16'\n",
"azure_storage_account = f'sa{suffix}'\n",
"storage_account_container = 'sqldatabasepackage'\n",
"sql_lcid = '1033'\n",
"sql_collation = 'SQL_Latin1_General_CP1_CI_AS'\n",
"vm_size = 'Standard_DS1_v2'"
"sql_collation = 'SQL_Latin1_General_CP1_CI_AS'"
],
"metadata": {
"azdata_cell_guid": "19ebeaf4-94c9-4d2b-bd9f-e3c6bf7f2dda",
@@ -260,34 +265,6 @@
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Create IoT hub"
],
"metadata": {
"azdata_cell_guid": "e37a04c3-515d-4cb7-99b2-f8bc6167510e"
}
},
{
"cell_type": "code",
"source": [
"hub_list = run_command(f'az iot hub list --resource-group {azure_resource_group}', returnObject=True)\n",
"hub_list = [hub for hub in hub_list if hub['name'] == iot_hub_name]\n",
"if len(hub_list) == 0:\n",
" run_command(f'az iot hub create --name {iot_hub_name} --resource-group {azure_resource_group} --location {azure_location} --sku {iot_hub_sku} --unit {iot_hub_units}')\n",
"else:\n",
" print(f'IoT hub \\\"{iot_hub_name}\\\" already exists')"
],
"metadata": {
"azdata_cell_guid": "f9f5e4ec-82a5-45df-a408-ddb0fb21847c",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
@@ -342,26 +319,25 @@
{
"cell_type": "markdown",
"source": [
"### Add the Edge device to the IoT hub"
"### Create network security group"
],
"metadata": {
"azdata_cell_guid": "fbc5f4ac-dfe0-4543-ace1-49b796251910"
"azdata_cell_guid": "b308771b-138a-40ce-a9d3-1d15094d537b"
}
},
{
"cell_type": "code",
"source": [
"device_list = run_command(f'az iot hub device-identity list --edge-enabled true --hub-name {iot_hub_name} --resource-group {azure_resource_group}', returnObject=True)\n",
"device_list = [device for device in device_list if device['deviceId'] == iot_device_id]\n",
"if len(device_list) == 0:\n",
" run_command(f'az iot hub device-identity create --device-id {iot_device_id} --hub-name {iot_hub_name} --resource-group {azure_resource_group} --edge-enabled true')\n",
"nsg_list = run_command(f'az network nsg list --resource-group {azure_resource_group}', returnObject=True)\n",
"nsg_list = [nsg for nsg in nsg_list if nsg['name'] == network_security_group]\n",
"if len(nsg_list) == 0:\n",
" run_command(f'az network nsg create --name {network_security_group} --resource-group {azure_resource_group} --location {azure_location}')\n",
" run_command(f'az network nsg rule create --name \\\"SQL\\\" --nsg-name {network_security_group} --priority 100 --resource-group {azure_resource_group} --access Allow --description \\\"Allow SQL\\\" --destination-address-prefixes \\\"*\\\" --destination-port-ranges {sql_port} --direction Inbound --source-address-prefixes Internet --protocol Tcp')\n",
"else:\n",
" print(f'Edge device \\\"{iot_device_id}\\\" already exists.')\n",
"connection_string = run_command(f'az iot hub device-identity show-connection-string --device-id {iot_device_id} --hub-name {iot_hub_name} --resource-group {azure_resource_group}', returnObject=True)\n",
"connection_string = connection_string['connectionString']"
" print(f'Network security group \\\"{network_security_group}\\\" already exists.')"
],
"metadata": {
"azdata_cell_guid": "c183c3e3-8699-4f29-993b-07bf848336e3",
"azdata_cell_guid": "99cbb95c-b109-4b2e-909b-ff71a62754fb",
"tags": [
"hide_input"
]
@@ -381,25 +357,103 @@
{
"cell_type": "code",
"source": [
"iot_deploy_result = run_command((f\"az deployment group create \"\r\n",
"f\"--resource-group {azure_resource_group} \"\r\n",
"f\"--template-uri \\\"https://aka.ms/iotedge-vm-deploy\\\" \"\r\n",
"f\"--parameters vmSize={vm_size} \"\r\n",
"f\"--parameters dnsLabelPrefix={iot_device_id} \"\r\n",
"f\"--parameters adminUsername={vm_admin} \"\r\n",
"f\"--parameters deviceConnectionString={connection_string} \"\r\n",
"f\"--parameters authenticationType=password \"\r\n",
"f\"--parameters adminPasswordOrKey=\\\"{vm_password}\\\"\"), returnObject=True)\r\n",
"vm_resource = [resource for resource in iot_deploy_result['properties']['dependencies'] if resource['resourceType'] == 'Microsoft.Compute/virtualMachines']\r\n",
"if len(vm_resource) != 1:\r\n",
" sys.exit('Failed to deploy the IoT Edge VM')\r\n",
"vm_name = vm_resource[0]['resourceName']\r\n",
"nsg_name = vm_name.replace('vm-','nsg-')\r\n",
"ip_address = run_command(f'az vm show -d -g {azure_resource_group} -n {vm_name} --query publicIps', returnObject=True)\r\n",
"run_command(f'az network nsg rule create --name \\\"SQL\\\" --nsg-name {nsg_name} --priority 100 --resource-group {azure_resource_group} --access Allow --description \\\"Allow SQL\\\" --destination-address-prefixes \\\"*\\\" --destination-port-ranges {sql_port} --direction Inbound --source-address-prefixes Internet --protocol Tcp')"
"vm_list = run_command(f'az vm list --resource-group {azure_resource_group}', returnObject=True)\n",
"vm_list = [vm for vm in vm_list if vm['name'] == iot_device_id]\n",
"if len(vm_list) == 0:\n",
" vm_image = run_command(f'az vm image list --all --location {azure_location} --offer iot_edge_vm_ubuntu --publisher microsoft_iot_edge --sku ubuntu_1604_edgeruntimeonly', returnObject=True)\n",
" image_urn = vm_image[0]['urn']\n",
" run_command(f'az vm image terms accept --urn {image_urn}')\n",
" vm_password_placeholder = '<admin_password>'\n",
" create_vm_command_template = f'az vm create --name {iot_device_id} --resource-group {azure_resource_group} --admin-username {vm_admin} --admin-password {vm_password_placeholder} --authentication-type password --image {image_urn} --location {azure_location} --nsg {network_security_group} --public-ip-address \\\"{public_ip_address_name}\\\" --public-ip-address-allocation static --public-ip-sku Standard --size {vm_size} --subnet {subnet_name} --subnet-address-prefix \\\"{subnet_address_prefix}\\\" --vnet-name {vnet_name} --vnet-address-prefix \\\"{vnet_address_prefix}\\\"'\n",
" run_command(create_vm_command_template.replace(vm_password_placeholder, vm_password), displayCommand=create_vm_command_template.replace(vm_password_placeholder, '******'))\n",
"else:\n",
" print(f'VM \\\"{iot_device_id}\\\" already exists, skipping the vm creation.')\n",
"ip_address = run_command(f'az vm show -d -g {azure_resource_group} -n {iot_device_id} --query publicIps', returnObject=True)"
],
"metadata": {
"azdata_cell_guid": "c8590c65-b274-460d-9659-97e81d2fd3ea",
"azdata_cell_guid": "157fc38f-cf2a-40c6-9c9e-88f45cc5c62f",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Create IoT hub"
],
"metadata": {
"azdata_cell_guid": "e37a04c3-515d-4cb7-99b2-f8bc6167510e"
}
},
{
"cell_type": "code",
"source": [
"hub_list = run_command(f'az iot hub list --resource-group {azure_resource_group}', returnObject=True)\n",
"hub_list = [hub for hub in hub_list if hub['name'] == iot_hub_name]\n",
"if len(hub_list) == 0:\n",
" run_command(f'az iot hub create --name {iot_hub_name} --resource-group {azure_resource_group} --location {azure_location} --sku {iot_hub_sku} --unit {iot_hub_units}')\n",
"else:\n",
" print(f'IoT hub \\\"{iot_hub_name}\\\" already exists')"
],
"metadata": {
"azdata_cell_guid": "f9f5e4ec-82a5-45df-a408-ddb0fb21847c",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Add the Edge device to the IoT hub"
],
"metadata": {
"azdata_cell_guid": "fbc5f4ac-dfe0-4543-ace1-49b796251910"
}
},
{
"cell_type": "code",
"source": [
"device_list = run_command(f'az iot hub device-identity list --edge-enabled true --hub-name {iot_hub_name} --resource-group {azure_resource_group}', returnObject=True)\n",
"device_list = [device for device in device_list if device['deviceId'] == iot_device_id]\n",
"if len(device_list) == 0:\n",
" run_command(f'az iot hub device-identity create --device-id {iot_device_id} --hub-name {iot_hub_name} --resource-group {azure_resource_group} --edge-enabled true')\n",
"else:\n",
" print(f'Edge device \\\"{iot_device_id}\\\" already exists.')"
],
"metadata": {
"azdata_cell_guid": "c183c3e3-8699-4f29-993b-07bf848336e3",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Configure Edge on the device"
],
"metadata": {
"azdata_cell_guid": "069db017-9169-499a-839b-9cd73ea7d01e"
}
},
{
"cell_type": "code",
"source": [
"connection_string = run_command(f'az iot hub device-identity show-connection-string --device-id {iot_device_id} --hub-name {iot_hub_name} --resource-group {azure_resource_group}', returnObject=True)\n",
"connection_string = connection_string['connectionString']\n",
"script = f'/etc/iotedge/configedge.sh \\'{connection_string}\\''\n",
"run_command(f'az vm run-command invoke -g {azure_resource_group} -n {iot_device_id} --command-id RunShellScript --script \\\"{script}\\\"')"
],
"metadata": {
"azdata_cell_guid": "9ec1e31a-79aa-49f4-a0e5-16f8d7c2dd21",
"tags": [
"hide_input"
]

View File

@@ -2,7 +2,7 @@
"name": "asde-deployment",
"displayName": "%extension-displayName%",
"description": "%extension-description%",
"version": "0.4.2",
"version": "0.4.1",
"publisher": "Microsoft",
"preview": true,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
@@ -10,7 +10,7 @@
"aiKey": "AIF-444c3af9-8e69-4462-ab49-4191e6ad1916",
"engines": {
"vscode": "*",
"azdata": ">=1.25.0"
"azdata": "*"
},
"repository": {
"type": "git",
@@ -43,13 +43,9 @@
"displayName": "%sql-edge-remote-display-name%"
},
{
"name": "azure-create-new-password-auth",
"name": "azure-create-new",
"displayName": "%sql-edge-azure-display-name%"
},
{
"name": "azure-create-new-sshkey-auth",
"displayName": "%sql-edge-azure-sshkey-display-name%"
},
{
"name": "azure-single-device",
"displayName": "%sql-edge-azure-single-device-display-name%"
@@ -275,7 +271,7 @@
"when": "type=remote"
},
{
"name": "sql-edge-azure-create-new-password-auth",
"name": "sql-edge_azure-create-new",
"dialog": {
"notebook": "./notebooks/edge/deploy-sql-edge-azure.ipynb",
"title": "%sql-edge-azure-title%",
@@ -403,129 +399,7 @@
"version": "2.13.0"
}
],
"when": "type=azure-create-new-password-auth"
},
{
"name": "sql-edge-azure-create-new-sshkey-auth",
"dialog": {
"notebook": "./notebooks/edge/deploy-sql-edge-azure-sshkey.ipynb",
"title": "%sql-edge-azure-title%",
"name": "sql-edge-azure-dialog",
"tabs": [
{
"title": "",
"sections": [
{
"title": "%azure-info-section-title%",
"collapsible": true,
"fields": [
{
"subscriptionVariableName": "AZDATA_NB_VAR_ASDE_SUBSCRIPTIONID",
"resourceGroupVariableName": "AZDATA_NB_VAR_ASDE_RESOURCEGROUP",
"type": "azure_account",
"required": true,
"allowNewResourceGroup": true,
"newResourceGroupFlagVariableName": "AZDATA_NB_VAR_ASDE_NEW_RESOURCEGROUP",
"newResourceGroupNameVariableName": "AZDATA_NB_VAR_ASDE_NEW_RESOURCEGROUP_NAME"
},
{
"type": "azure_locations",
"label": "%azure_location%",
"defaultValue": "westus",
"required": true,
"locationVariableName": "AZDATA_NB_VAR_ASDE_AZURE_LOCATION",
"locations": [
"australiaeast",
"australiasoutheast",
"brazilsouth",
"canadacentral",
"canadaeast",
"centralindia",
"centralus",
"eastasia",
"eastus",
"eastus2",
"francecentral",
"japaneast",
"japanwest",
"koreacentral",
"koreasouth",
"northcentralus",
"northeurope",
"southcentralus",
"southindia",
"southeastasia",
"uksouth",
"ukwest",
"westcentralus",
"westeurope",
"westus",
"westus2"
]
},
{
"label": "%vm_admin%",
"variableName": "AZDATA_NB_VAR_ASDE_VM_ADMIN",
"type": "text",
"required": true
},
{
"label": "%vm_ssh_public_key%",
"variableName": "AZDATA_NB_VAR_ASDE_VM_PASSWORD",
"type": "text",
"required": true
}
]
},
{
"title": "%sqlserver-info-section-title%",
"collapsible": true,
"fields": [
{
"label": "%docker-sql-password-field%",
"variableName": "AZDATA_NB_VAR_SA_PASSWORD",
"type": "sql_password",
"userName": "sa",
"confirmationRequired": true,
"confirmationLabel": "%docker-confirm-sql-password-field%",
"defaultValue": "",
"required": true
},
{
"label": "%docker-sql-port-field%",
"variableName": "AZDATA_NB_VAR_ASDE_SQL_PORT",
"type": "number",
"defaultValue": 31433,
"required": true
},
{
"label": "%package_path%",
"description": "%package_path_description%",
"variableName": "AZDATA_NB_VAR_ASDE_PACKAGE_PATH",
"type": "file_picker",
"required": false,
"filter": {
"displayName": "%package-files%",
"fileTypes": [
"zip",
"bacpac",
"dacpac"
]
}
}
]
}
]
}
]
},
"requiredTools": [
{
"name": "azure-cli",
"version": "2.13.0"
}
],
"when": "type=azure-create-new-sshkey-auth"
"when": "type=azure-create-new"
},
{
"name": "sql-edge_azure-single-device",

View File

@@ -25,8 +25,7 @@
"edge-remote-target-field": "Name or IP address",
"edge-remote-username-field": "Username",
"edge-remote-password-field": "Password",
"sql-edge-azure-display-name": "New Azure IoT Hub and VM (password authentication)",
"sql-edge-azure-sshkey-display-name": "New Azure IoT Hub and VM (ssh public key authentication)",
"sql-edge-azure-display-name": "New Azure IoT Hub and VM",
"sql-edge-azure-title": "Deploy Azure SQL Edge to a new Azure VM via IoT hub",
"azure_subscription_id": "Subscription id",
"azure_resource_group": "Resource group",
@@ -49,6 +48,5 @@
"sql-edge-azure-multi-device-display-name": "Multiple devices of an Azure IoT Hub",
"sql-edge-azure-multi-device-title": "Deploy Azure SQL Edge to multiple Azure IoT devices",
"device-target-condition": "Target condition",
"device-target-condition-learn-more": "Learn more about target condition",
"vm_ssh_public_key": "SSH public key"
"device-target-condition-learn-more": "Learn more about target condition"
}

View File

@@ -2,7 +2,7 @@
"name": "azdata",
"displayName": "%azdata.displayName%",
"description": "%azdata.description%",
"version": "0.6.2",
"version": "0.6.0",
"publisher": "Microsoft",
"preview": true,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
@@ -107,12 +107,7 @@
"when": "azdata.found"
}
]
},
"resourceDeploymentOptionsSources": [
{
"id": "arc.controller.config.profiles"
}
]
}
},
"dependencies": {
"request": "^2.88.2",

View File

@@ -5,26 +5,13 @@
import * as azdataExt from 'azdata-ext';
import * as vscode from 'vscode';
import { IAzdataTool, isEulaAccepted, MIN_AZDATA_VERSION, promptForEula } from './azdata';
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';
/**
* Validates that :
* - Azdata is installed
* - The Azdata version is >= the minimum required version
* - The Azdata CLI has been accepted
* @param azdata The azdata tool to check
* @param eulaAccepted Whether the Azdata CLI EULA has been accepted
*/
async function validateAzdata(azdata: IAzdataTool | undefined, eulaAccepted: boolean): Promise<void> {
throwIfNoAzdataOrEulaNotAccepted(azdata, eulaAccepted);
await throwIfRequiredVersionMissing(azdata);
}
export function throwIfNoAzdataOrEulaNotAccepted(azdata: IAzdataTool | undefined, eulaAccepted: boolean): asserts azdata {
throwIfNoAzdata(azdata);
if (!eulaAccepted) {
@@ -33,13 +20,6 @@ export function throwIfNoAzdataOrEulaNotAccepted(azdata: IAzdataTool | undefined
}
}
export async function throwIfRequiredVersionMissing(azdata: IAzdataTool): Promise<void> {
const currentVersion = await azdata.getSemVersion();
if (currentVersion.compare(MIN_AZDATA_VERSION) < 0) {
throw new Error(loc.missingRequiredVersion(MIN_AZDATA_VERSION.raw));
}
}
export function throwIfNoAzdata(localAzdata: IAzdataTool | undefined): asserts localAzdata {
if (!localAzdata) {
Logger.log(loc.noAzdata);
@@ -75,47 +55,47 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
profileName?: string,
storageClass?: string,
additionalEnvVars?: azdataExt.AdditionalEnvVars,
azdataContext?: string) => {
session?: azdataExt.AzdataSession) => {
await localAzdataDiscovered;
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.dc.create(namespace, name, connectivityMode, resourceGroup, location, subscription, profileName, storageClass, additionalEnvVars, azdataContext);
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.dc.create(namespace, name, connectivityMode, resourceGroup, location, subscription, profileName, storageClass, additionalEnvVars, session);
},
endpoint: {
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
await localAzdataDiscovered;
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.dc.endpoint.list(additionalEnvVars, azdataContext);
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.dc.endpoint.list(additionalEnvVars, session);
}
},
config: {
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
await localAzdataDiscovered;
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.dc.config.list(additionalEnvVars, azdataContext);
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.dc.config.list(additionalEnvVars, session);
},
show: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
show: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
await localAzdataDiscovered;
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.dc.config.show(additionalEnvVars, azdataContext);
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.dc.config.show(additionalEnvVars, session);
}
}
},
postgres: {
server: {
delete: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
delete: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
await localAzdataDiscovered;
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.postgres.server.delete(name, additionalEnvVars, azdataContext);
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.postgres.server.delete(name, additionalEnvVars, session);
},
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
await localAzdataDiscovered;
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.postgres.server.list(additionalEnvVars, azdataContext);
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.postgres.server.list(additionalEnvVars, session);
},
show: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
show: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
await localAzdataDiscovered;
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.postgres.server.show(name, additionalEnvVars, azdataContext);
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.postgres.server.show(name, additionalEnvVars, session);
},
edit: async (
name: string,
@@ -132,30 +112,31 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
replaceEngineSettings?: boolean;
workers?: number;
},
engineVersion?: string,
additionalEnvVars?: azdataExt.AdditionalEnvVars,
azdataContext?: string) => {
session?: azdataExt.AzdataSession) => {
await localAzdataDiscovered;
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.postgres.server.edit(name, args, additionalEnvVars, azdataContext);
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.postgres.server.edit(name, args, engineVersion, additionalEnvVars, session);
}
}
},
sql: {
mi: {
delete: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
delete: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
await localAzdataDiscovered;
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.sql.mi.delete(name, additionalEnvVars, azdataContext);
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.sql.mi.delete(name, additionalEnvVars, session);
},
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
await localAzdataDiscovered;
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.sql.mi.list(additionalEnvVars, azdataContext);
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.sql.mi.list(additionalEnvVars, session);
},
show: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
show: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
await localAzdataDiscovered;
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.sql.mi.show(name, additionalEnvVars, azdataContext);
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.sql.mi.show(name, additionalEnvVars, session);
},
edit: async (
name: string,
@@ -167,11 +148,11 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
noWait?: boolean;
},
additionalEnvVars?: azdataExt.AdditionalEnvVars,
azdataContext?: string
session?: azdataExt.AzdataSession
) => {
await localAzdataDiscovered;
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.sql.mi.edit(name, args, additionalEnvVars, azdataContext);
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.sql.mi.edit(name, args, additionalEnvVars, session);
}
}
}
@@ -181,9 +162,13 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
throwIfNoAzdata(azdataToolService.localAzdata);
return azdataToolService.localAzdata.getPath();
},
login: async (endpointOrNamespace: azdataExt.EndpointOrNamespace, username: string, password: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.login(endpointOrNamespace, username, password, additionalEnvVars, azdataContext);
login: async (endpoint: string, username: string, password: string, additionalEnvVars?: azdataExt.AdditionalEnvVars) => {
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.login(endpoint, username, password, additionalEnvVars);
},
acquireSession: async (endpoint: string, username: string, password: string, additionEnvVars?: azdataExt.AdditionalEnvVars) => {
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata?.acquireSession(endpoint, username, password, additionEnvVars);
},
getSemVersion: async () => {
await localAzdataDiscovered;

View File

@@ -13,15 +13,11 @@ import { getPlatformDownloadLink, getPlatformReleaseVersion } from './azdataRele
import { executeCommand, executeSudoCommand, ExitCodeError, ProcessOutput } from './common/childProcess';
import { HttpClient } from './common/httpClient';
import Logger from './common/logger';
import { Deferred } from './common/promise';
import { getErrorMessage, NoAzdataError, searchForCmd } from './common/utils';
import { azdataAcceptEulaKey, azdataConfigSection, azdataFound, azdataInstallKey, azdataUpdateKey, debugConfigKey, eulaAccepted, eulaUrl, microsoftPrivacyStatementUrl } from './constants';
import * as loc from './localizedConstants';
/**
* The minimum required azdata CLI version for this extension to function properly
*/
export const MIN_AZDATA_VERSION = new SemVer('20.3.2');
export const enum AzdataDeployOption {
dontPrompt = 'dontPrompt',
prompt = 'prompt'
@@ -36,7 +32,20 @@ export interface IAzdataTool extends azdataExt.IAzdataApi {
* @param args The args to pass to azdata
* @param parseResult A function used to parse out the raw result into the desired shape
*/
executeCommand<R>(args: string[], additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<R>>
executeCommand<R>(args: string[], additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataOutput<R>>
}
class AzdataSession implements azdataExt.AzdataSession {
private _session = new Deferred<void>();
public sessionEnded(): Promise<void> {
return this._session.promise;
}
public dispose(): void {
this._session.resolve();
}
}
/**
@@ -45,6 +54,9 @@ export interface IAzdataTool extends azdataExt.IAzdataApi {
export class AzdataTool implements azdataExt.IAzdataApi {
private _semVersion: SemVer;
private _currentSession: azdataExt.AzdataSession | undefined = undefined;
private _currentlyExecutingCommands: Deferred<void>[] = [];
private _queuedCommands: { deferred: Deferred<void>, session?: azdataExt.AzdataSession }[] = [];
constructor(private _path: string, version: string) {
this._semVersion = new SemVer(version);
@@ -78,7 +90,7 @@ export class AzdataTool implements azdataExt.IAzdataApi {
profileName?: string,
storageClass?: string,
additionalEnvVars?: azdataExt.AdditionalEnvVars,
azdataContext?: string): Promise<azdataExt.AzdataOutput<void>> => {
session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<void>> => {
const args = ['arc', 'dc', 'create',
'--namespace', namespace,
'--name', name,
@@ -92,32 +104,32 @@ export class AzdataTool implements azdataExt.IAzdataApi {
if (storageClass) {
args.push('--storage-class', storageClass);
}
return this.executeCommand<void>(args, additionalEnvVars, azdataContext);
return this.executeCommand<void>(args, additionalEnvVars, session);
},
endpoint: {
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.DcEndpointListResult[]>> => {
return this.executeCommand<azdataExt.DcEndpointListResult[]>(['arc', 'dc', 'endpoint', 'list'], additionalEnvVars, azdataContext);
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.DcEndpointListResult[]>> => {
return this.executeCommand<azdataExt.DcEndpointListResult[]>(['arc', 'dc', 'endpoint', 'list'], additionalEnvVars, session);
}
},
config: {
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigListResult[]>> => {
return this.executeCommand<azdataExt.DcConfigListResult[]>(['arc', 'dc', 'config', 'list'], additionalEnvVars, azdataContext);
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigListResult[]>> => {
return this.executeCommand<azdataExt.DcConfigListResult[]>(['arc', 'dc', 'config', 'list'], additionalEnvVars, session);
},
show: (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigShowResult>> => {
return this.executeCommand<azdataExt.DcConfigShowResult>(['arc', 'dc', 'config', 'show'], additionalEnvVars, azdataContext);
show: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigShowResult>> => {
return this.executeCommand<azdataExt.DcConfigShowResult>(['arc', 'dc', 'config', 'show'], additionalEnvVars, session);
}
}
},
postgres: {
server: {
delete: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<void>> => {
return this.executeCommand<void>(['arc', 'postgres', 'server', 'delete', '-n', name, '--force'], additionalEnvVars, azdataContext);
delete: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<void>> => {
return this.executeCommand<void>(['arc', 'postgres', 'server', 'delete', '-n', name, '--force'], additionalEnvVars, session);
},
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerListResult[]>> => {
return this.executeCommand<azdataExt.PostgresServerListResult[]>(['arc', 'postgres', 'server', 'list'], additionalEnvVars, azdataContext);
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerListResult[]>> => {
return this.executeCommand<azdataExt.PostgresServerListResult[]>(['arc', 'postgres', 'server', 'list'], additionalEnvVars, session);
},
show: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerShowResult>> => {
return this.executeCommand<azdataExt.PostgresServerShowResult>(['arc', 'postgres', 'server', 'show', '-n', name], additionalEnvVars, azdataContext);
show: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerShowResult>> => {
return this.executeCommand<azdataExt.PostgresServerShowResult>(['arc', 'postgres', 'server', 'show', '-n', name], additionalEnvVars, session);
},
edit: (
name: string,
@@ -134,8 +146,9 @@ export class AzdataTool implements azdataExt.IAzdataApi {
replaceEngineSettings?: boolean,
workers?: number
},
engineVersion?: string,
additionalEnvVars?: azdataExt.AdditionalEnvVars,
azdataContext?: string): Promise<azdataExt.AzdataOutput<void>> => {
session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<void>> => {
const argsArray = ['arc', 'postgres', 'server', 'edit', '-n', name];
if (args.adminPassword) { argsArray.push('--admin-password'); }
if (args.coresLimit) { argsArray.push('--cores-limit', args.coresLimit); }
@@ -148,20 +161,21 @@ export class AzdataTool implements azdataExt.IAzdataApi {
if (args.port) { argsArray.push('--port', args.port.toString()); }
if (args.replaceEngineSettings) { argsArray.push('--replace-engine-settings'); }
if (args.workers) { argsArray.push('--workers', args.workers.toString()); }
return this.executeCommand<void>(argsArray, additionalEnvVars, azdataContext);
if (engineVersion) { argsArray.push('--engine-version', engineVersion); }
return this.executeCommand<void>(argsArray, additionalEnvVars, session);
}
}
},
sql: {
mi: {
delete: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<void>> => {
return this.executeCommand<void>(['arc', 'sql', 'mi', 'delete', '-n', name], additionalEnvVars, azdataContext);
delete: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<void>> => {
return this.executeCommand<void>(['arc', 'sql', 'mi', 'delete', '-n', name], additionalEnvVars, session);
},
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiListResult[]>> => {
return this.executeCommand<azdataExt.SqlMiListResult[]>(['arc', 'sql', 'mi', 'list'], additionalEnvVars, azdataContext);
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiListResult[]>> => {
return this.executeCommand<azdataExt.SqlMiListResult[]>(['arc', 'sql', 'mi', 'list'], additionalEnvVars, session);
},
show: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiShowResult>> => {
return this.executeCommand<azdataExt.SqlMiShowResult>(['arc', 'sql', 'mi', 'show', '-n', name], additionalEnvVars, azdataContext);
show: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiShowResult>> => {
return this.executeCommand<azdataExt.SqlMiShowResult>(['arc', 'sql', 'mi', 'show', '-n', name], additionalEnvVars, session);
},
edit: (
name: string,
@@ -172,7 +186,8 @@ export class AzdataTool implements azdataExt.IAzdataApi {
memoryRequest?: string,
noWait?: boolean,
},
additionalEnvVars?: azdataExt.AdditionalEnvVars
additionalEnvVars?: azdataExt.AdditionalEnvVars,
session?: azdataExt.AzdataSession
): Promise<azdataExt.AzdataOutput<void>> => {
const argsArray = ['arc', 'sql', 'mi', 'edit', '-n', name];
if (args.coresLimit) { argsArray.push('--cores-limit', args.coresLimit); }
@@ -180,22 +195,59 @@ export class AzdataTool implements azdataExt.IAzdataApi {
if (args.memoryLimit) { argsArray.push('--memory-limit', args.memoryLimit); }
if (args.memoryRequest) { argsArray.push('--memory-request', args.memoryRequest); }
if (args.noWait) { argsArray.push('--no-wait'); }
return this.executeCommand<void>(argsArray, additionalEnvVars);
return this.executeCommand<void>(argsArray, additionalEnvVars, session);
}
}
}
};
public async login(endpointOrNamespace: azdataExt.EndpointOrNamespace, username: string, password: string, additionalEnvVars: azdataExt.AdditionalEnvVars = {}, azdataContext?: string): Promise<azdataExt.AzdataOutput<void>> {
const args = ['login', '-u', username];
if (endpointOrNamespace.endpoint) {
args.push('-e', endpointOrNamespace.endpoint);
} else if (endpointOrNamespace.namespace) {
args.push('--namespace', endpointOrNamespace.namespace);
} else {
throw new Error(loc.endpointOrNamespaceRequired);
public async login(endpoint: string, username: string, password: string, additionalEnvVars: azdataExt.AdditionalEnvVars = {}): Promise<azdataExt.AzdataOutput<void>> {
// Since login changes the context we want to wait until all currently executing commands are finished before this is executed
while (this._currentlyExecutingCommands.length > 0) {
await this._currentlyExecutingCommands[0];
}
return this.executeCommand<void>(args, Object.assign({}, additionalEnvVars, { 'AZDATA_PASSWORD': password }), azdataContext);
// Logins need to be done outside the session aware logic so call impl directly
return this.executeCommandImpl<void>(['login', '-e', endpoint, '-u', username], Object.assign({}, additionalEnvVars, { 'AZDATA_PASSWORD': password }));
}
public async acquireSession(endpoint: string, username: string, password: string, additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataSession> {
const session = new AzdataSession();
session.sessionEnded().then(async () => {
// Wait for all commands running for this session to end
while (this._currentlyExecutingCommands.length > 0) {
await this._currentlyExecutingCommands[0].promise;
}
this._currentSession = undefined;
// Start our next command now that we're all done with this session
// TODO: Should we check if the command has a session that hasn't started? That should never happen..
// TODO: Look into kicking off multiple commands
this._queuedCommands.shift()?.deferred.resolve();
});
// We're not in a session or waiting on anything so just set the current session right now
if (!this._currentSession && this._queuedCommands.length === 0) {
this._currentSession = session;
} else {
// We're in a session or another command is executing so add this to the end of the queued commands and wait our turn
const deferred = new Deferred<void>();
deferred.promise.then(() => {
this._currentSession = session;
// We've started a new session so look at all our queued commands and start
// the ones for this session now.
this._queuedCommands = this._queuedCommands.filter(c => {
if (c.session === this._currentSession) {
c.deferred.resolve();
return false;
}
return true;
});
});
this._queuedCommands.push({ deferred, session: undefined });
await deferred.promise;
}
await this.login(endpoint, username, password, additionalEnvVars);
return session;
}
/**
@@ -213,16 +265,34 @@ export class AzdataTool implements azdataExt.IAzdataApi {
};
}
public async executeCommand<R>(args: string[], additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<R>> {
if (this._currentSession && this._currentSession !== session) {
const deferred = new Deferred<void>();
this._queuedCommands.push({ deferred, session: session });
await deferred.promise;
}
const executingDeferred = new Deferred<void>();
this._currentlyExecutingCommands.push(executingDeferred);
try {
return await this.executeCommandImpl<R>(args, additionalEnvVars);
}
finally {
this._currentlyExecutingCommands = this._currentlyExecutingCommands.filter(c => c !== executingDeferred);
executingDeferred.resolve();
// If there isn't an active session and we still have queued commands then we have to manually kick off the next one
if (this._queuedCommands.length > 0 && !this._currentSession) {
this._queuedCommands.shift()?.deferred.resolve();
}
}
}
/**
* Executes the specified azdata command.
* Executes the specified azdata command. This is NOT session-aware so should only be used for calls that don't care about a session
* @param args The args to pass to azdata
* @param additionalEnvVars Additional environment variables to set for this execution
*/
public async executeCommand<R>(args: string[], additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<R>> {
private async executeCommandImpl<R>(args: string[], additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataOutput<R>> {
try {
if (azdataContext) {
args = args.concat('--controller-context', azdataContext);
}
const output = JSON.parse((await executeAzdataCommand(`"${this._path}"`, args.concat(['--output', 'json']), additionalEnvVars)).stdout);
return {
logs: <string[]>output.log,
@@ -372,22 +442,8 @@ export async function checkAndInstallAzdata(userRequested: boolean = false): Pro
export async function checkAndUpdateAzdata(currentAzdata?: IAzdataTool, userRequested: boolean = false): Promise<boolean> {
if (currentAzdata !== undefined) {
const newSemVersion = await discoverLatestAvailableAzdataVersion();
const currentSemVersion = await currentAzdata.getSemVersion();
Logger.log(loc.foundAzdataVersionToUpdateTo(newSemVersion.raw, currentSemVersion.raw));
if (MIN_AZDATA_VERSION.compare(currentSemVersion) === 1) {
if (newSemVersion.compare(MIN_AZDATA_VERSION) >= 0) {
return await promptToUpdateAzdata(newSemVersion.raw, userRequested, true);
} else {
// This should never happen - it means that the currently available version to download
// is < the version we require. If this was to happen it'd imply something is wrong with
// the version JSON or the minimum required version.
// Regardless, there's nothing we can do and so we just bail out at this point and tell the user
// they have to install it manually (hopefully it's available and wasn't a publishing mistake)
vscode.window.showInformationMessage(loc.requiredVersionNotAvailable(MIN_AZDATA_VERSION.raw, newSemVersion.raw));
Logger.log(loc.requiredVersionNotAvailable(newSemVersion.raw, currentSemVersion.raw));
}
}
else if (newSemVersion.compare(currentSemVersion) === 1) {
if (newSemVersion.compare(await currentAzdata.getSemVersion()) === 1) {
Logger.log(loc.foundAzdataVersionToUpdateTo(newSemVersion.raw, (await currentAzdata.getSemVersion()).raw));
return await promptToUpdateAzdata(newSemVersion.raw, userRequested);
} else {
Logger.log(loc.currentlyInstalledVersionIsLatest((await currentAzdata.getSemVersion()).raw));
@@ -448,65 +504,39 @@ async function promptToInstallAzdata(userRequested: boolean = false): Promise<bo
* @param newVersion - provides the new version that the user will be prompted to update to
* @param userRequested - if true this operation was requested in response to a user issued command, if false it was issued at startup by system
* returns true if update was done and false otherwise.
* @param required - Whether this update is required. If true then we will always show the prompt and warn the user if they decline it
*/
async function promptToUpdateAzdata(newVersion: string, userRequested: boolean = false, required = false): Promise<boolean> {
if (required) {
let response: string | undefined = loc.yes;
const responses = [loc.yes, loc.no];
Logger.log(loc.promptForRequiredAzdataUpdateLog(MIN_AZDATA_VERSION.raw, newVersion));
response = await vscode.window.showInformationMessage(loc.promptForRequiredAzdataUpdate(MIN_AZDATA_VERSION.raw, newVersion), ...responses);
async function promptToUpdateAzdata(newVersion: string, userRequested: boolean = false): Promise<boolean> {
let response: string | undefined = loc.yes;
const config = <AzdataDeployOption>getConfig(azdataUpdateKey);
if (userRequested) {
Logger.show();
Logger.log(loc.userRequestedUpdate);
}
if (config === AzdataDeployOption.dontPrompt && !userRequested) {
Logger.log(loc.skipUpdate(config));
return false;
}
const responses = userRequested
? [loc.yes, loc.no]
: [loc.yes, loc.askLater, loc.doNotAskAgain];
if (config === AzdataDeployOption.prompt) {
Logger.log(loc.promptForAzdataUpdateLog(newVersion));
response = await vscode.window.showInformationMessage(loc.promptForAzdataUpdate(newVersion), ...responses);
Logger.log(loc.userResponseToUpdatePrompt(response));
if (response === loc.yes) {
try {
await updateAzdata();
vscode.window.showInformationMessage(loc.azdataUpdated(newVersion));
Logger.log(loc.azdataUpdated(newVersion));
return true;
} catch (err) {
// Windows: 1602 is User cancelling installation/update - not unexpected so don't display
if (!(err instanceof ExitCodeError) || err.code !== 1602) {
vscode.window.showWarningMessage(loc.updateError(err));
Logger.log(loc.updateError(err));
}
}
} else {
vscode.window.showWarningMessage(loc.missingRequiredVersion(MIN_AZDATA_VERSION.raw));
}
} else {
let response: string | undefined = loc.yes;
const config = <AzdataDeployOption>getConfig(azdataUpdateKey);
if (userRequested) {
Logger.show();
Logger.log(loc.userRequestedUpdate);
}
if (config === AzdataDeployOption.dontPrompt && !userRequested) {
Logger.log(loc.skipUpdate(config));
return false;
}
const responses = userRequested
? [loc.yes, loc.no]
: [loc.yes, loc.askLater, loc.doNotAskAgain];
if (config === AzdataDeployOption.prompt) {
Logger.log(loc.promptForAzdataUpdateLog(newVersion));
response = await vscode.window.showInformationMessage(loc.promptForAzdataUpdate(newVersion), ...responses);
Logger.log(loc.userResponseToUpdatePrompt(response));
}
if (response === loc.doNotAskAgain) {
await setConfig(azdataUpdateKey, AzdataDeployOption.dontPrompt);
} else if (response === loc.yes) {
try {
await updateAzdata();
vscode.window.showInformationMessage(loc.azdataUpdated(newVersion));
Logger.log(loc.azdataUpdated(newVersion));
return true;
} catch (err) {
// Windows: 1602 is User cancelling installation/update - not unexpected so don't display
if (!(err instanceof ExitCodeError) || err.code !== 1602) {
vscode.window.showWarningMessage(loc.updateError(err));
Logger.log(loc.updateError(err));
}
}
if (response === loc.doNotAskAgain) {
await setConfig(azdataUpdateKey, AzdataDeployOption.dontPrompt);
} else if (response === loc.yes) {
try {
await updateAzdata();
vscode.window.showInformationMessage(loc.azdataUpdated(newVersion));
Logger.log(loc.azdataUpdated(newVersion));
return true;
} catch (err) {
// Windows: 1602 is User cancelling installation/update - not unexpected so don't display
if (!(err instanceof ExitCodeError) || err.code !== 1602) {
vscode.window.showWarningMessage(loc.updateError(err));
Logger.log(loc.updateError(err));
}
}
}

View File

@@ -38,11 +38,8 @@ export const promptLog = (logEntry: string) => localize('azdata.promptLog', "Pro
export const promptForAzdataInstall = localize('azdata.couldNotFindAzdataWithPrompt', "Could not find Azure Data CLI, install it now? If not then some features will not be able to function.");
export const promptForAzdataInstallLog = promptLog(promptForAzdataInstall);
export const promptForAzdataUpdate = (version: string): string => localize('azdata.promptForAzdataUpdate', "A new version of Azure Data CLI ( {0} ) is available, do you wish to update to it now?", version);
export const promptForRequiredAzdataUpdate = (requiredVersion: string, latestVersion: string): string => localize('azdata.promptForRequiredAzdataUpdate', "This extension requires Azure Data CLI >= {0} to be installed, do you wish to update to the latest version ({1}) now? If you do not then some functionality may not work.", requiredVersion, latestVersion);
export const requiredVersionNotAvailable = (requiredVersion: string, currentVersion: string): string => localize('azdata.requiredVersionNotAvailable', "This extension requires Azure Data CLI >= {0} to be installed, but the current version available is only {1}. Install the correct version manually from [here](https://docs.microsoft.com/sql/azdata/install/deploy-install-azdata) and then restart Azure Data Studio.", requiredVersion, currentVersion);
export const promptForAzdataUpdateLog = (version: string): string => promptLog(promptForAzdataUpdate(version));
export const promptForRequiredAzdataUpdateLog = (requiredVersion: string, latestVersion: string): string => promptLog(promptForRequiredAzdataUpdate(requiredVersion, latestVersion));
export const missingRequiredVersion = (requiredVersion: string): string => localize('azdata.missingRequiredVersion', "Azure Data CLI >= {0} is required for this extension to function, some features may not work correctly until that version or higher is installed.", requiredVersion);
export const downloadError = localize('azdata.downloadError', "Error while downloading");
export const installError = (err: any): string => localize('azdata.installError', "Error installing Azure Data CLI: {0}", err.message ?? err);
export const updateError = (err: any): string => localize('azdata.updateError', "Error updating Azure Data CLI: {0}", err.message ?? err);
@@ -69,4 +66,3 @@ export const promptForEula = (privacyStatementUrl: string, eulaUrl: string) => l
export const promptForEulaLog = (privacyStatementUrl: string, eulaUrl: string) => promptLog(promptForEula(privacyStatementUrl, eulaUrl));
export const userResponseToEulaPrompt = (response: string | undefined) => localize('azdata.promptForEulaResponse', "User response to EULA prompt: {0}", response);
export const eulaAcceptedStateOnStartup = (eulaAccepted: boolean) => localize('azdata.eulaAcceptedStateOnStartup', "'EULA Accepted' state on startup: {0}", eulaAccepted);
export const endpointOrNamespaceRequired = localize('azdata.endpointOrNamespaceRequired', "Either an endpoint or a namespace must be specified");

View File

@@ -51,7 +51,7 @@ describe('api', function (): void {
it('succeed when azdata present and EULA accepted', async function (): Promise<void> {
const mementoMock = TypeMoq.Mock.ofType<vscode.Memento>();
mementoMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => true);
const azdataTool = new AzdataTool('', '99.0.0');
const azdataTool = new AzdataTool('', '1.0.0');
const azdataToolService = new AzdataToolService();
azdataToolService.localAzdata = azdataTool;
// Not using a mock here because it'll hang when resolving mocked objects
@@ -60,7 +60,7 @@ describe('api', function (): void {
sinon.stub(childProcess, 'executeCommand').callsFake(async (_command, args) => {
// Version needs to be valid so it can be parsed correctly
if (args[0] === '--version') {
return { stdout: `99.0.0`, stderr: '' };
return { stdout: `1.0.0`, stderr: '' };
}
console.log(args[0]);
return { stdout: `{ }`, stderr: '' };
@@ -96,8 +96,15 @@ describe('api', function (): void {
async function assertApiCalls(api: azdataExt.IExtension, assertCallback: (promise: Promise<any>, message: string) => Promise<void>): Promise<void> {
await assertCallback(api.azdata.getPath(), 'getPath');
await assertCallback(api.azdata.getSemVersion(), 'getSemVersion');
await assertCallback(api.azdata.login({ endpoint: 'https://127.0.0.1' }, '', ''), 'login');
await assertCallback(api.azdata.login({ namespace: 'namespace' }, '', ''), 'login');
await assertCallback(api.azdata.login('', '', ''), 'login');
await assertCallback((async () => {
let session: azdataExt.AzdataSession | undefined;
try {
session = await api.azdata.acquireSession('', '', '');
} finally {
session?.dispose();
}
})(), 'acquireSession');
await assertCallback(api.azdata.version(), 'version');
await assertCallback(api.azdata.arc.dc.create('', '', '', '', '', ''), 'arc dc create');
@@ -110,7 +117,7 @@ describe('api', function (): void {
await assertCallback(api.azdata.arc.sql.mi.list(), 'arc sql mi list');
await assertCallback(api.azdata.arc.sql.mi.delete(''), 'arc sql mi delete');
await assertCallback(api.azdata.arc.sql.mi.show(''), 'arc sql mi show');
await assertCallback(api.azdata.arc.sql.mi.edit('', {}), 'arc sql mi edit');
await assertCallback(api.azdata.arc.sql.mi.edit('', { }), 'arc sql mi edit');
await assertCallback(api.azdata.arc.postgres.server.list(), 'arc sql postgres server list');
await assertCallback(api.azdata.arc.postgres.server.delete(''), 'arc sql postgres server delete');
await assertCallback(api.azdata.arc.postgres.server.show(''), 'arc sql postgres server show');

View File

@@ -3,6 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdataExt from 'azdata-ext';
import * as should from 'should';
import * as sinon from 'sinon';
import * as vscode from 'vscode';
@@ -16,8 +17,9 @@ import * as fs from 'fs';
import { AzdataReleaseInfo } from '../azdataReleaseInfo';
import * as TypeMoq from 'typemoq';
import { eulaAccepted } from '../constants';
import { sleep } from './testUtils';
const oldAzdataMock = new azdata.AzdataTool('/path/to/azdata', azdata.MIN_AZDATA_VERSION.raw);
const oldAzdataMock = new azdata.AzdataTool('/path/to/azdata', '0.0.0');
const currentAzdataMock = new azdata.AzdataTool('/path/to/azdata', '9999.999.999');
/**
@@ -220,10 +222,120 @@ describe('azdata', function () {
const endpoint = 'myEndpoint';
const username = 'myUsername';
const password = 'myPassword';
await azdataTool.login({ endpoint: endpoint }, username, password);
await azdataTool.login(endpoint, username, password);
verifyExecuteCommandCalledWithArgs(['login', endpoint, username]);
});
describe('acquireSession', function (): void {
it('calls login', async function (): Promise<void> {
const endpoint = 'myEndpoint';
const username = 'myUsername';
const password = 'myPassword';
const session = await azdataTool.acquireSession(endpoint, username, password);
session.dispose();
verifyExecuteCommandCalledWithArgs(['login', endpoint, username]);
});
it('command executed under current session completes', async function (): Promise<void> {
const session = await azdataTool.acquireSession('', '', '');
try {
await azdataTool.arc.dc.config.show(undefined, session);
} finally {
session.dispose();
}
verifyExecuteCommandCalledWithArgs(['login'], 0);
verifyExecuteCommandCalledWithArgs(['arc', 'dc', 'config', 'show'], 1);
});
it('multiple commands executed under current session completes', async function (): Promise<void> {
const session = await azdataTool.acquireSession('', '', '');
try {
// Kick off multiple commands at the same time and then ensure that they both complete
await Promise.all([
azdataTool.arc.dc.config.show(undefined, session),
azdataTool.arc.sql.mi.list(undefined, session)
]);
} finally {
session.dispose();
}
verifyExecuteCommandCalledWithArgs(['login'], 0);
verifyExecuteCommandCalledWithArgs(['arc', 'dc', 'config', 'show'], 1);
verifyExecuteCommandCalledWithArgs(['arc', 'sql', 'mi', 'list'], 2);
});
it('command executed without session context is queued up until session is closed', async function (): Promise<void> {
const session = await azdataTool.acquireSession('', '', '');
let nonSessionCommand: Promise<any> | undefined = undefined;
try {
// Start one command in the current session
await azdataTool.arc.dc.config.show(undefined, session);
// Verify that the command isn't executed until after the session is disposed
let isFulfilled = false;
nonSessionCommand = azdataTool.arc.sql.mi.list().then(() => isFulfilled = true);
await sleep(2000);
should(isFulfilled).equal(false, 'The command should not be completed yet');
} finally {
session.dispose();
}
await nonSessionCommand;
verifyExecuteCommandCalledWithArgs(['login'], 0);
verifyExecuteCommandCalledWithArgs(['arc', 'dc', 'config', 'show'], 1);
verifyExecuteCommandCalledWithArgs(['arc', 'sql', 'mi', 'list'], 2);
});
it('multiple commands executed without session context are queued up until session is closed', async function (): Promise<void> {
const session = await azdataTool.acquireSession('', '', '');
let nonSessionCommand1: Promise<any> | undefined = undefined;
let nonSessionCommand2: Promise<any> | undefined = undefined;
try {
// Start one command in the current session
await azdataTool.arc.dc.config.show(undefined, session);
// Verify that neither command is completed until the session is closed
let isFulfilled = false;
nonSessionCommand1 = azdataTool.arc.sql.mi.list().then(() => isFulfilled = true);
nonSessionCommand2 = azdataTool.arc.postgres.server.list().then(() => isFulfilled = true);
await sleep(2000);
should(isFulfilled).equal(false, 'The commands should not be completed yet');
} finally {
session.dispose();
}
await Promise.all([nonSessionCommand1, nonSessionCommand2]);
verifyExecuteCommandCalledWithArgs(['login'], 0);
verifyExecuteCommandCalledWithArgs(['arc', 'dc', 'config', 'show'], 1);
verifyExecuteCommandCalledWithArgs(['arc', 'sql', 'mi', 'list'], 2);
verifyExecuteCommandCalledWithArgs(['arc', 'postgres', 'server', 'list'], 3);
});
it('attempting to acquire a second session while a first is still active queues the second session', async function (): Promise<void> {
const firstSession = await azdataTool.acquireSession('', '', '');
let sessionPromise: Promise<azdataExt.AzdataSession> | undefined = undefined;
let secondSessionCommand: Promise<any> | undefined = undefined;
try {
try {
// Start one command in the current session
await azdataTool.arc.dc.config.show(undefined, firstSession);
// Verify that none of the commands for the second session are completed before the first is disposed
let isFulfilled = false;
sessionPromise = azdataTool.acquireSession('', '', '');
sessionPromise.then(session => {
isFulfilled = true;
secondSessionCommand = azdataTool.arc.sql.mi.list(undefined, session).then(() => isFulfilled = true);
});
await sleep(2000);
should(isFulfilled).equal(false, 'The commands should not be completed yet');
} finally {
firstSession.dispose();
}
} finally {
(await sessionPromise)?.dispose();
}
should(secondSessionCommand).not.equal(undefined, 'The second command should have been queued already');
await secondSessionCommand!;
verifyExecuteCommandCalledWithArgs(['login'], 0);
verifyExecuteCommandCalledWithArgs(['arc', 'dc', 'config', 'show'], 1);
verifyExecuteCommandCalledWithArgs(['login'], 2);
verifyExecuteCommandCalledWithArgs(['arc', 'sql', 'mi', 'list'], 3);
});
});
it('version', async function (): Promise<void> {
executeCommandStub.resolves({ stdout: '1.0.0', stderr: '' });
await azdataTool.version();
@@ -665,7 +777,7 @@ async function testDarwinSkippedUpdateDontPrompt() {
async function testWin32SkippedUpdateDontPrompt() {
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
await azdata.checkAndUpdateAzdata(oldAzdataMock);
should(executeSudoCommandStub.notCalled).be.true(`executeSudoCommand should not have been called ${executeSudoCommandStub.getCalls().join(os.EOL)}`);
should(executeSudoCommandStub.notCalled).be.true('executeSudoCommand should not have been called');
}
async function testLinuxSkippedUpdateDontPrompt() {

View File

@@ -18,3 +18,7 @@ export async function assertRejected(promise: Promise<any>, message: string): Pr
throw new Error(message);
}
export async function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

View File

@@ -160,7 +160,7 @@ declare module 'azdata-ext' {
export interface PostgresServerShowResult {
apiVersion: string, // "arcdata.microsoft.com/v1alpha1"
kind: string, // "postgresql"
kind: string, // "postgresql-12"
metadata: {
creationTimestamp: string, // "2020-08-19T20:25:11Z"
generation: number, // 1
@@ -177,8 +177,7 @@ declare module 'azdata-ext' {
}[],
settings: {
default: { [key: string]: string } // { "max_connections": "101", "work_mem": "4MB" }
},
version: string // "12"
}
},
scale: {
shards: number, // 1 (shards was renamed to workers, kept here for backwards compatibility)
@@ -245,27 +244,25 @@ declare module 'azdata-ext' {
code?: number
}
export interface EndpointOrNamespace {
endpoint?: string,
namespace?: string
}
export interface AzdataSession extends vscode.Disposable { }
export interface IAzdataApi {
arc: {
dc: {
create(namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<void>>,
create(namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string, additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<void>>,
endpoint: {
list(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<DcEndpointListResult[]>>
list(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<DcEndpointListResult[]>>
},
config: {
list(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<DcConfigListResult[]>>,
show(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<DcConfigShowResult>>
list(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<DcConfigListResult[]>>,
show(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<DcConfigShowResult>>
}
},
postgres: {
server: {
delete(name: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<void>>,
list(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<PostgresServerListResult[]>>,
show(name: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<PostgresServerShowResult>>,
delete(name: string, additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<void>>,
list(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<PostgresServerListResult[]>>,
show(name: string, additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<PostgresServerShowResult>>,
edit(
name: string,
args: {
@@ -281,16 +278,17 @@ declare module 'azdata-ext' {
replaceEngineSettings?: boolean,
workers?: number
},
engineVersion?: string,
additionalEnvVars?: AdditionalEnvVars,
azdataContext?: string
session?: AzdataSession
): Promise<AzdataOutput<void>>
}
},
sql: {
mi: {
delete(name: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<void>>,
list(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<SqlMiListResult[]>>,
show(name: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<SqlMiShowResult>>,
delete(name: string, additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<void>>,
list(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<SqlMiListResult[]>>,
show(name: string, additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<SqlMiShowResult>>,
edit(
name: string,
args: {
@@ -301,13 +299,22 @@ declare module 'azdata-ext' {
noWait?: boolean,
},
additionalEnvVars?: AdditionalEnvVars,
azdataContext?: string
session?: AzdataSession
): Promise<AzdataOutput<void>>
}
}
},
getPath(): Promise<string>,
login(endpointOrNamespace: EndpointOrNamespace, username: string, password: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<void>>,
login(endpoint: string, username: string, password: string, additionalEnvVars?: AdditionalEnvVars): Promise<AzdataOutput<void>>,
/**
* Acquires a session for the specified controller, which will log in to the specified controller and then block all other commands
* that are not part of the original session from executing until the session is released (disposed).
* @param endpoint
* @param username
* @param password
* @param additionalEnvVars
*/
acquireSession(endpoint: string, username: string, password: string, additionalEnvVars?: AdditionalEnvVars): Promise<AzdataSession>,
/**
* 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

View File

@@ -317,11 +317,6 @@
}
]
},
"resourceDeploymentValueProviders": [
{
"id": "subscription-id-to-tenant-id"
}
],
"hasAzureResourceProviders": true
},
"dependencies": {

View File

@@ -12,10 +12,10 @@ import {
AzureAccount,
AzureAccountProviderMetadata,
AzureAuthType,
Deferred,
Resource,
Tenant
} from 'azurecore';
import { Deferred } from '../interfaces';
} from '../interfaces';
import * as url from 'url';
import { SimpleTokenCache } from '../simpleTokenCache';
@@ -161,7 +161,7 @@ export abstract class AzureAuth implements vscode.Disposable {
const tenant = account.properties.tenants.find(t => t.id === tenantId);
if (!tenant) {
throw new AzureAuthError(localize('azure.tenantNotFound', "Specified tenant with ID '{0}' not found.", tenantId), `Tenant ${tenantId} not found.`, undefined);
throw new AzureAuthError(localize('azure.tenantNotFound', "Specifed tenant with ID '{0}' not found.", tenantId), `Tenant ${tenantId} not found.`, undefined);
}
const cachedTokens = await this.getSavedToken(tenant, resource, account.key);

View File

@@ -4,8 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { AuthorizationCodePostData, AzureAuth, OAuthTokenResponse } from './azureAuth';
import { AzureAccountProviderMetadata, AzureAuthType, Resource, Tenant } from 'azurecore';
import { Deferred } from '../interfaces';
import { AzureAccountProviderMetadata, AzureAuthType, Deferred, Resource, Tenant } from '../interfaces';
import * as vscode from 'vscode';
import * as crypto from 'crypto';
import { SimpleTokenCache } from '../simpleTokenCache';

View File

@@ -18,9 +18,12 @@ import {
AzureAccountProviderMetadata,
AzureAuthType,
Tenant,
Resource
} from 'azurecore';
import { Deferred } from '../interfaces';
Resource,
Deferred,
// Tenant,
// Subscription
} from '../interfaces';
import { SimpleTokenCache } from '../simpleTokenCache';
import { Logger } from '../../utils/Logger';
const localize = nls.loadMessageBundle();

View File

@@ -10,9 +10,9 @@ import * as nls from 'vscode-nls';
import {
AzureAccountProviderMetadata,
AzureAuthType,
Deferred,
AzureAccount
} from 'azurecore';
import { Deferred } from './interfaces';
} from './interfaces';
import { SimpleTokenCache } from './simpleTokenCache';
import { Logger } from '../utils/Logger';
@@ -107,14 +107,14 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
return this._getSecurityToken(account, resource);
}
getAccountSecurityToken(account: azdata.Account, tenantId: string, resource: azdata.AzureResource): Thenable<Token | undefined> {
return this._getAccountSecurityToken(account, tenantId, resource);
getAccountSecurityToken(account: azdata.Account, tenant: string, resource: azdata.AzureResource): Thenable<Token | undefined> {
return this._getAccountSecurityToken(account, tenant, resource);
}
private async _getAccountSecurityToken(account: azdata.Account, tenantId: string, resource: azdata.AzureResource): Promise<Token | undefined> {
private async _getAccountSecurityToken(account: azdata.Account, tenant: string, resource: azdata.AzureResource): Promise<Token | undefined> {
await this.initCompletePromise;
const azureAuth = this.getAuthMethod(undefined);
return azureAuth?.getAccountSecurityToken(account, tenantId, resource);
return azureAuth?.getAccountSecurityToken(account, tenant, resource);
}
private async _getSecurityToken(account: azdata.Account, resource: azdata.AzureResource): Promise<MultiTenantTokenResponse | undefined> {

View File

@@ -10,8 +10,7 @@ import * as vscode from 'vscode';
import { SimpleTokenCache } from './simpleTokenCache';
import providerSettings from './providerSettings';
import { AzureAccountProvider as AzureAccountProvider } from './azureAccountProvider';
import { AzureAccountProviderMetadata } from 'azurecore';
import { ProviderSettings } from './interfaces';
import { AzureAccountProviderMetadata, ProviderSettings } from './interfaces';
import * as loc from '../localizedConstants';
let localize = nls.loadMessageBundle();

View File

@@ -3,7 +3,129 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azurecore from 'azurecore';
import * as azdata from 'azdata';
/**
* Represents a tenant (an Azure Active Directory instance) to which a user has access
*/
export interface Tenant {
/**
* Globally unique identifier of the tenant
*/
id: string;
/**
* Display name of the tenant
*/
displayName: string;
/**
* Identifier of the user in the tenant
*/
userId?: string;
/**
* The category the user has set their tenant to (e.g. Home Tenant)
*/
tenantCategory?: string;
}
/**
* Represents a resource exposed by an Azure Active Directory
*/
export interface Resource {
/**
* Identifier of the resource
*/
id: string;
/**
* Endpoint url used to access the resource
*/
endpoint: string;
/**
* Resource ID for azdata
*/
azureResourceId?: azdata.AzureResource
}
/**
* Represents settings for an AAD account provider
*/
interface Settings {
/**
* Host of the authority
*/
host?: string;
/**
* Identifier of the client application
*/
clientId?: string;
/**
* Information that describes the Microsoft resource management resource
*/
microsoftResource?: Resource
/**
* Information that describes the AAD graph resource
*/
graphResource?: Resource;
/**
* Information that describes the MS graph resource
*/
msGraphResource?: Resource;
/**
* Information that describes the Azure resource management resource
*/
armResource?: Resource;
/**
* Information that describes the SQL Azure resource
*/
sqlResource?: Resource;
/**
* Information that describes the OSS RDBMS resource
*/
ossRdbmsResource?: Resource;
/**
* Information that describes the Azure Key Vault resource
*/
azureKeyVaultResource?: Resource;
/**
* Information that describes the Azure Dev Ops resource
*/
azureDevOpsResource?: Resource;
/**
* A list of tenant IDs to authenticate against. If defined, then these IDs will be used
* instead of querying the tenants endpoint of the armResource
*/
adTenants?: string[];
// AuthorizationCodeGrantFlowSettings //////////////////////////////////
/**
* An optional site ID that brands the interactive aspect of sign in
*/
siteId?: string;
/**
* Redirect URI that is used to signify the end of the interactive aspect of sign it
*/
redirectUri?: string;
scopes?: string[]
portalEndpoint?: string
}
/**
* Mapping of configuration key with the metadata to instantiate the account provider
@@ -17,7 +139,44 @@ export interface ProviderSettings {
/**
* Metadata for the provider
*/
metadata: azurecore.AzureAccountProviderMetadata;
metadata: AzureAccountProviderMetadata;
}
/**
* Extension of account provider metadata to override settings type for Azure account providers
*/
export interface AzureAccountProviderMetadata extends azdata.AccountProviderMetadata {
/**
* Azure specific account provider settings.
*/
settings: Settings;
}
export enum AzureAuthType {
AuthCodeGrant = 0,
DeviceCode = 1
}
/**
* Properties specific to an Azure account
*/
interface AzureAccountProperties {
/**
* Auth type of azure used to authenticate this account.
*/
azureAuthType?: AzureAuthType
providerSettings: AzureAccountProviderMetadata;
/**
* Whether or not the account is a Microsoft account
*/
isMsAccount: boolean;
/**
* A list of tenants (aka directories) that the account belongs to
*/
tenants: Tenant[];
}
export interface Subscription {
@@ -26,6 +185,16 @@ export interface Subscription {
displayName: string
}
/**
* Override of the Account type to enforce properties that are AzureAccountProperties
*/
export interface AzureAccount extends azdata.Account {
/**
* AzureAccountProperties specifically used for Azure accounts
*/
properties: AzureAccountProperties;
}
/**
* Token returned from a request for an access token
*/

View File

@@ -71,39 +71,6 @@ declare module 'azureResource' {
export interface AzureResourceResourceGroup extends AzureResource {
}
export interface AzureLocation {
id: string,
name: string,
displayName: string,
regionalDisplayName: string,
metadata: {
regionType: string,
regionCategory: string,
geographyGroup: string,
longitude: number,
latitude: number,
physicalLocation: string,
pairedRegion: {
name: string,
id: string,
}[],
},
}
export interface AzureSqlManagedInstance extends AzureGraphResource {
}
export interface ManagedDatabase {
id: string,
location: string,
name: string,
properties: {
sourceDatabaseId: string,
status: string
},
type: string
}
export interface AzureResourceDatabase extends AzureSqlResource {
serverName: string;
serverFullName: string;

View File

@@ -17,7 +17,7 @@ import { AzureResourceTreeProvider } from './tree/treeProvider';
import { AzureResourceAccountTreeNode } from './tree/accountTreeNode';
import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService, IAzureTerminalService } from '../azureResource/interfaces';
import { AzureResourceServiceNames } from './constants';
import { AzureAccount, Tenant } from 'azurecore';
import { AzureAccount, Tenant } from '../account-provider/interfaces';
import { FlatAccountTreeNode } from './tree/flatAccountTreeNode';
import { ConnectionDialogTreeProvider } from './tree/connectionDialogTreeProvider';

View File

@@ -8,7 +8,7 @@ import * as msRest from '@azure/ms-rest-js';
import { Account } from 'azdata';
import { azureResource } from 'azureResource';
import { AzureAccount, Tenant } from 'azurecore';
import { AzureAccount, Tenant } from '../account-provider/interfaces';
export interface IAzureResourceSubscriptionService {
getSubscriptions(account: Account, credential: msRest.ServiceClientCredentials, tenantId: string): Promise<azureResource.AzureResourceSubscription[]>;

View File

@@ -9,7 +9,7 @@ import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import * as WS from 'ws';
import { IAzureTerminalService } from '../interfaces';
import { AzureAccount, Tenant } from 'azurecore';
import { AzureAccount, Tenant } from '../../account-provider/interfaces';
const localize = nls.loadMessageBundle();

View File

@@ -21,7 +21,7 @@ import { AzureResourceMessageTreeNode } from '../messageTreeNode';
import { AzureResourceErrorMessageUtil } from '../utils';
import { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService } from '../../azureResource/interfaces';
import { AzureAccount } from 'azurecore';
import { AzureAccount } from '../../account-provider/interfaces';
export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNodeBase {
public constructor(

View File

@@ -19,7 +19,7 @@ import { AzureResourceItemType, AzureResourceServiceNames } from '../constants';
import { AzureResourceMessageTreeNode } from '../messageTreeNode';
import { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService } from '../../azureResource/interfaces';
import { AzureAccount } from 'azurecore';
import { AzureAccount } from '../../account-provider/interfaces';
import { AzureResourceService } from '../resourceService';
import { AzureResourceResourceTreeNode } from '../resourceTreeNode';
import { AzureResourceErrorMessageUtil } from '../utils';

View File

@@ -7,7 +7,7 @@ import { ResourceGraphClient } from '@azure/arm-resourcegraph';
import { TokenCredentials } from '@azure/ms-rest-js';
import axios, { AxiosRequestConfig } from 'axios';
import * as azdata from 'azdata';
import { AzureRestResponse, GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult, GetBlobContainersResult, GetFileSharesResult, HttpRequestMethod, GetLocationsResult, GetManagedDatabasesResult } from 'azurecore';
import { AzureRestResponse, GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult, GetBlobContainersResult, GetFileSharesResult, HttpRequestMethod } from 'azurecore';
import { azureResource } from 'azureResource';
import { EOL } from 'os';
import * as nls from 'vscode-nls';
@@ -142,40 +142,6 @@ export async function getResourceGroups(appContext: AppContext, account?: azdata
return result;
}
export async function getLocations(appContext: AppContext, account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors: boolean = false): Promise<GetLocationsResult> {
const result: GetLocationsResult = { locations: [], errors: [] };
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants) || !subscription) {
const error = new Error(invalidAzureAccount);
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
return result;
}
await Promise.all(account.properties.tenants.map(async (tenant: { id: string; }) => {
try {
const path = `/subscriptions/${subscription.id}/locations?api-version=2020-01-01`;
const response = await makeHttpRequest(account, subscription, path, HttpRequestMethod.GET, undefined, ignoreErrors);
result.locations.push(...response.response.data.value);
result.errors.push(...response.errors);
} catch (err) {
const error = new Error(localize('azure.accounts.getLocations.queryError', "Error fetching locations for account {0} ({1}) subscription {2} ({3}) tenant {4} : {5}",
account.displayInfo.displayName,
account.displayInfo.userId,
subscription.id,
subscription.name,
tenant.id,
err instanceof Error ? err.message : err));
console.warn(error);
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
}
}));
return result;
}
export async function runResourceQuery<T extends azureResource.AzureGraphResource>(
account: azdata.Account,
subscriptions: azureResource.AzureResourceSubscription[],
@@ -429,15 +395,6 @@ export async function makeHttpRequest(account: azdata.Account, subscription: azu
return result;
}
export async function getManagedDatabases(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, managedInstance: azureResource.AzureSqlManagedInstance, ignoreErrors: boolean): Promise<GetManagedDatabasesResult> {
const path = `/subscriptions/${subscription.id}/resourceGroups/${managedInstance.resourceGroup}/providers/Microsoft.Sql/managedInstances/${managedInstance.name}/databases?api-version=2020-02-02-preview`;
const response = await makeHttpRequest(account, subscription, path, HttpRequestMethod.GET, undefined, ignoreErrors);
return {
databases: response?.response?.data?.value ?? [],
errors: response.errors ? response.errors : []
};
}
export async function getBlobContainers(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors: boolean): Promise<GetBlobContainersResult> {
const path = `/subscriptions/${subscription.id}/resourceGroups/${storageAccount.resourceGroup}/providers/Microsoft.Storage/storageAccounts/${storageAccount.name}/blobServices/default/containers?api-version=2019-06-01`;
const response = await makeHttpRequest(account, subscription, path, HttpRequestMethod.GET, undefined, ignoreErrors);

View File

@@ -17,175 +17,6 @@ declare module 'azurecore' {
name = 'Microsoft.azurecore'
}
/**
* Override of the Account type to enforce properties that are AzureAccountProperties
*/
export interface AzureAccount extends azdata.Account {
/**
* AzureAccountProperties specifically used for Azure accounts
*/
properties: AzureAccountProperties;
}
/**
* Properties specific to an Azure account
*/
export interface AzureAccountProperties {
/**
* Auth type of azure used to authenticate this account.
*/
azureAuthType?: AzureAuthType
providerSettings: AzureAccountProviderMetadata;
/**
* Whether or not the account is a Microsoft account
*/
isMsAccount: boolean;
/**
* A list of tenants (aka directories) that the account belongs to
*/
tenants: Tenant[];
}
export const enum AzureAuthType {
AuthCodeGrant = 0,
DeviceCode = 1
}
/**
* Extension of account provider metadata to override settings type for Azure account providers
*/
export interface AzureAccountProviderMetadata extends azdata.AccountProviderMetadata {
/**
* Azure specific account provider settings.
*/
settings: Settings;
}
/**
* Represents settings for an AAD account provider
*/
interface Settings {
/**
* Host of the authority
*/
host?: string;
/**
* Identifier of the client application
*/
clientId?: string;
/**
* Information that describes the Microsoft resource management resource
*/
microsoftResource?: Resource
/**
* Information that describes the AAD graph resource
*/
graphResource?: Resource;
/**
* Information that describes the MS graph resource
*/
msGraphResource?: Resource;
/**
* Information that describes the Azure resource management resource
*/
armResource?: Resource;
/**
* Information that describes the SQL Azure resource
*/
sqlResource?: Resource;
/**
* Information that describes the OSS RDBMS resource
*/
ossRdbmsResource?: Resource;
/**
* Information that describes the Azure Key Vault resource
*/
azureKeyVaultResource?: Resource;
/**
* Information that describes the Azure Dev Ops resource
*/
azureDevOpsResource?: Resource;
/**
* A list of tenant IDs to authenticate against. If defined, then these IDs will be used
* instead of querying the tenants endpoint of the armResource
*/
adTenants?: string[];
// AuthorizationCodeGrantFlowSettings //////////////////////////////////
/**
* An optional site ID that brands the interactive aspect of sign in
*/
siteId?: string;
/**
* Redirect URI that is used to signify the end of the interactive aspect of sign it
*/
redirectUri?: string;
scopes?: string[]
portalEndpoint?: string
}
/**
* Represents a resource exposed by an Azure Active Directory
*/
export interface Resource {
/**
* Identifier of the resource
*/
id: string;
/**
* Endpoint url used to access the resource
*/
endpoint: string;
/**
* Resource ID for azdata
*/
azureResourceId?: azdata.AzureResource
}
/**
* Represents a tenant (an Azure Active Directory instance) to which a user has access
*/
export interface Tenant {
/**
* Globally unique identifier of the tenant
*/
id: string;
/**
* Display name of the tenant
*/
displayName: string;
/**
* Identifier of the user in the tenant
*/
userId?: string;
/**
* The category the user has set their tenant to (e.g. Home Tenant)
*/
tenantCategory?: string;
}
/**
* Enumeration of the Azure datacenter regions. See https://docs.microsoft.com/dotnet/api/microsoft.azure.management.resourcemanager.fluent.core.region
*/
@@ -245,9 +76,7 @@ declare module 'azurecore' {
export interface IExtension {
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly?: boolean): Promise<GetSubscriptionsResult>;
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Promise<GetResourceGroupsResult>;
getLocations(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Promise<GetLocationsResult>;
getSqlManagedInstances(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetSqlManagedInstancesResult>;
getManagedDatabases(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, managedInstance: azureResource.AzureSqlManagedInstance, ignoreErrors?: boolean): Promise<GetManagedDatabasesResult>;
getSqlServers(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetSqlServersResult>;
getSqlVMServers(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetSqlVMServersResult>;
getStorageAccounts(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetStorageAccountResult>;
@@ -277,9 +106,7 @@ declare module 'azurecore' {
export type GetSubscriptionsResult = { subscriptions: azureResource.AzureResourceSubscription[], errors: Error[] };
export type GetResourceGroupsResult = { resourceGroups: azureResource.AzureResourceResourceGroup[], errors: Error[] };
export type GetLocationsResult = { locations: azureResource.AzureLocation[], errors: Error[] };
export type GetSqlManagedInstancesResult = { resources: azureResource.AzureSqlManagedInstance[], errors: Error[] };
export type GetManagedDatabasesResult = { databases: azureResource.ManagedDatabase[], errors: Error[] };
export type GetSqlManagedInstancesResult = { resources: azureResource.AzureGraphResource[], errors: Error[] };
export type GetSqlServersResult = { resources: azureResource.AzureGraphResource[], errors: Error[] };
export type GetSqlVMServersResult = { resources: azureResource.AzureGraphResource[], errors: Error[] };
export type GetStorageAccountResult = { resources: azureResource.AzureGraphResource[], errors: Error[] };

View File

@@ -147,11 +147,6 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
: azureResourceUtils.getSubscriptions(appContext, account, ignoreErrors);
},
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Promise<azurecore.GetResourceGroupsResult> { return azureResourceUtils.getResourceGroups(appContext, account, subscription, ignoreErrors); },
getLocations(account?: azdata.Account,
subscription?: azureResource.AzureResourceSubscription,
ignoreErrors?: boolean): Promise<azurecore.GetLocationsResult> {
return azureResourceUtils.getLocations(appContext, account, subscription, ignoreErrors);
},
provideResources(): azureResource.IAzureResourceProvider[] {
const arcFeaturedEnabled = vscode.workspace.getConfiguration(constants.extensionConfigSectionName).get('enableArcFeatures');
const providers: azureResource.IAzureResourceProvider[] = [
@@ -174,12 +169,6 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
ignoreErrors: boolean): Promise<azurecore.GetSqlManagedInstancesResult> {
return azureResourceUtils.runResourceQuery(account, subscriptions, ignoreErrors, `where type == "${azureResource.AzureResourceType.sqlManagedInstance}"`);
},
getManagedDatabases(account: azdata.Account,
subscription: azureResource.AzureResourceSubscription,
managedInstance: azureResource.AzureSqlManagedInstance,
ignoreErrors: boolean): Promise<azurecore.GetManagedDatabasesResult> {
return azureResourceUtils.getManagedDatabases(account, subscription, managedInstance, ignoreErrors);
},
getSqlServers(account: azdata.Account,
subscriptions: azureResource.AzureResourceSubscription[],
ignoreErrors: boolean): Promise<azurecore.GetSqlServersResult> {

View File

@@ -5,10 +5,14 @@
import * as should from 'should';
import * as TypeMoq from 'typemoq';
// import * as azdata from 'azdata';
// import * as vscode from 'vscode';
// import * as sinon from 'sinon';
import 'mocha';
import { AzureAuthCodeGrant } from '../../../account-provider/auths/azureAuthCodeGrant';
// import { AzureDeviceCode } from '../../../account-provider/auths/azureDeviceCode';
import { Token, TokenClaims, AccessToken, RefreshToken, OAuthTokenResponse, TokenPostData } from '../../../account-provider/auths/azureAuth';
import { Tenant, AzureAccount } from 'azurecore'
import { Tenant, AzureAccount } from '../../../account-provider/interfaces';
import providerSettings from '../../../account-provider/providerSettings';
import { AzureResource } from 'azdata';
import { AxiosResponse } from 'axios';

View File

@@ -14,7 +14,7 @@ import { azureResource } from 'azureResource';
import { AzureResourceDatabaseTreeDataProvider } from '../../../../azureResource/providers/database/databaseTreeDataProvider';
import { AzureResourceItemType } from '../../../../azureResource/constants';
import { IAzureResourceService } from '../../../../azureResource/interfaces';
import { AzureAccount } from 'azurecore';
import { AzureAccount } from '../../../../account-provider/interfaces';
import settings from '../../../../account-provider/providerSettings';
// Mock services

View File

@@ -19,7 +19,7 @@ import { IAzureResourceService } from '../../../../azureResource/interfaces';
let mockDatabaseServerService: TypeMoq.IMock<IAzureResourceService<azureResource.AzureResourceDatabaseServer>>;
let mockExtensionContext: TypeMoq.IMock<vscode.ExtensionContext>;
import settings from '../../../../account-provider/providerSettings';
import { AzureAccount } from 'azurecore';
import { AzureAccount } from '../../../../account-provider/interfaces';
// Mock test data
const mockAccount: AzureAccount = {

View File

@@ -10,7 +10,7 @@ import { fail } from 'assert';
import { azureResource } from 'azureResource';
import { AzureResourceService } from '../../azureResource/resourceService';
import { AzureAccount } from 'azurecore';
import { AzureAccount } from '../../account-provider/interfaces';
import settings from '../../account-provider/providerSettings';
// Mock test data

View File

@@ -14,7 +14,7 @@ import { AzureResourceResourceTreeNode } from '../../azureResource/resourceTreeN
import { AppContext } from '../../appContext';
import { AzureResourceServiceNames } from '../../azureResource/constants';
import settings from '../../account-provider/providerSettings';
import { AzureAccount } from 'azurecore';
import { AzureAccount } from '../../account-provider/interfaces';
// Mock test data
const mockAccount: AzureAccount = {

View File

@@ -12,7 +12,6 @@
"azdata": "*"
},
"activationEvents": [
"onCommand:azdata.resource.deploy",
"onCommand:bigDataClusters.command.mount",
"onCommand:bigDataClusters.command.refreshmount",
"onCommand:bigDataClusters.command.deletemount",
@@ -179,174 +178,6 @@
"contents": "%bdc.view.welcome.loading%",
"when": "!bdc.loaded"
}
],
"resourceDeploymentTypes": [
{
"name": "sql-bdc",
"displayIndex": 3,
"displayName": "%resource-type-sql-bdc-display-name%",
"description": "%resource-type-sql-bdc-description%",
"platforms": "*",
"icon": "./images/sql_bdc.svg",
"tags": [
"On-premises",
"SQL Server",
"Cloud"
],
"options": [
{
"name": "version",
"displayName": "%version-display-name%",
"values": [
{
"name": "bdc2019",
"displayName": "%bdc-2019-display-name%"
}
]
},
{
"name": "target",
"displayName": "%bdc-deployment-target%",
"values": [
{
"name": "new-aks",
"displayName": "%bdc-deployment-target-new-aks%"
},
{
"name": "existing-aks",
"displayName": "%bdc-deployment-target-existing-aks%"
},
{
"name": "existing-kubeadm",
"displayName": "%bdc-deployment-target-existing-kubeadm%"
},
{
"name": "existing-aro",
"displayName": "%bdc-deployment-target-existing-aro%"
},
{
"name": "existing-openshift",
"displayName": "%bdc-deployment-target-existing-openshift%"
}
]
}
],
"providers": [
{
"name": "sql-bdc_new-aks_bdc2019",
"bdcWizard": {
"type": "new-aks",
"notebook": "./notebooks/deployment/2019/deploy-bdc-aks.ipynb"
},
"requiredTools": [
{
"name": "kubectl",
"version": "1.13.0"
},
{
"name": "azure-cli"
},
{
"name": "azdata-old",
"version": "20.3.2"
}
],
"when": "target=new-aks&&version=bdc2019"
},
{
"name": "sql-bdc_existing-aks_bdc2019",
"bdcWizard": {
"type": "existing-aks",
"notebook": "./notebooks/deployment/2019/deploy-bdc-existing-aks.ipynb"
},
"requiredTools": [
{
"name": "kubectl",
"version": "1.13.0"
},
{
"name": "azdata-old",
"version": "20.3.2"
}
],
"when": "target=existing-aks&&version=bdc2019"
},
{
"name": "sql-bdc_existing-kubeadm_bdc2019",
"bdcWizard": {
"type": "existing-kubeadm",
"notebook": "./notebooks/deployment/2019/deploy-bdc-existing-kubeadm.ipynb"
},
"requiredTools": [
{
"name": "kubectl",
"version": "1.13.0"
},
{
"name": "azdata-old",
"version": "20.3.2"
}
],
"when": "target=existing-kubeadm&&version=bdc2019"
},
{
"name": "sql-bdc_existing-aro_bdc2019",
"bdcWizard": {
"type": "existing-aro",
"notebook": "./notebooks/deployment/2019/deploy-bdc-existing-aro.ipynb"
},
"requiredTools": [
{
"name": "kubectl",
"version": "1.13.0"
},
{
"name": "azdata-old",
"version": "20.3.2"
}
],
"when": "target=existing-aro&&version=bdc2019"
},
{
"name": "sql-bdc_existing-openshift_bdc2019",
"bdcWizard": {
"type": "existing-openshift",
"notebook": "./notebooks/deployment/2019/deploy-bdc-existing-openshift.ipynb"
},
"requiredTools": [
{
"name": "kubectl",
"version": "1.13.0"
},
{
"name": "azdata-old",
"version": "20.3.2"
}
],
"when": "target=existing-openshift&&version=bdc2019"
}
],
"agreements": [
{
"template": "%bdc-agreement%",
"links": [
{
"text": "%microsoft-privacy-statement%",
"url": "https://go.microsoft.com/fwlink/?LinkId=853010"
},
{
"text": "%bdc-agreement-bdc-eula%",
"url": "https://go.microsoft.com/fwlink/?LinkId=2002534"
},
{
"text": "%bdc-agreement-azdata-eula%",
"url": "https://aka.ms/eula-azdata-en"
}
],
"when": "true"
}
]
}
]
},
"dependencies": {

View File

@@ -12,35 +12,5 @@
"bdc.configuration.title": "Big Data Cluster",
"bdc.view.welcome.connect": "No SQL Big Data Cluster controllers registered. [Learn More](https://docs.microsoft.com/sql/big-data-cluster/big-data-cluster-overview)\n[Connect Controller](command:bigDataClusters.command.connectController)",
"bdc.view.welcome.loading": "Loading controllers...",
"bdc.ignoreSslVerification.desc": "Ignore SSL verification errors against SQL Server Big Data Cluster endpoints such as HDFS, Spark, and Controller if true",
"resource-type-sql-bdc-display-name": "SQL Server Big Data Cluster",
"resource-type-sql-bdc-description": "SQL Server Big Data Cluster allows you to deploy scalable clusters of SQL Server, Spark, and HDFS containers running on Kubernetes",
"version-display-name": "Version",
"bdc-2019-display-name": "SQL Server 2019",
"bdc-deployment-target": "Deployment target",
"bdc-deployment-target-new-aks": "New Azure Kubernetes Service Cluster",
"bdc-deployment-target-existing-aks": "Existing Azure Kubernetes Service Cluster",
"bdc-deployment-target-existing-kubeadm": "Existing Kubernetes Cluster (kubeadm)",
"bdc-deployment-target-existing-aro": "Existing Azure Red Hat OpenShift cluster",
"bdc-deployment-target-existing-openshift": "Existing OpenShift cluster",
"bdc-cluster-settings-section-title": "SQL Server Big Data Cluster settings",
"bdc-cluster-name-field": "Cluster name",
"bdc-controller-username-field": "Controller username",
"bdc-password-field": "Password",
"bdc-confirm-password-field": "Confirm password",
"bdc-azure-settings-section-title": "Azure settings",
"bdc-azure-subscription-id-field": "Subscription id",
"bdc-azure-subscription-id-placeholder": "Use my default Azure subscription",
"bdc-azure-resource-group-field": "Resource group name",
"bdc-azure-region-field": "Region",
"bdc-azure-aks-name-field": "AKS cluster name",
"bdc-azure-vm-size-field": "VM size",
"bdc-azure-vm-count-field": "VM count",
"bdc-storage-class-field": "Storage class name",
"bdc-data-size-field": "Capacity for data (GB)",
"bdc-log-size-field": "Capacity for logs (GB)",
"bdc-agreement": "I accept {0}, {1} and {2}.",
"microsoft-privacy-statement": "Microsoft Privacy Statement",
"bdc-agreement-azdata-eula": "azdata License Terms",
"bdc-agreement-bdc-eula": "SQL Server License Terms"
"bdc.ignoreSslVerification.desc": "Ignore SSL verification errors against SQL Server Big Data Cluster endpoints such as HDFS, Spark, and Controller if true"
}

View File

@@ -2,7 +2,7 @@
"name": "dacpac",
"displayName": "SQL Server Dacpac",
"description": "SQL Server Dacpac for Azure Data Studio.",
"version": "1.9.0",
"version": "1.8.0",
"publisher": "Microsoft",
"preview": false,
"engines": {

View File

@@ -311,8 +311,7 @@ export function createViewContext(): ViewTestContext {
tabbedPanel: undefined!,
separator: undefined!,
propertiesContainer: undefined!,
infoBox: undefined!,
slider: undefined!
infoBox: undefined!
}
};
return {

View File

@@ -22,7 +22,7 @@ export class TestDeployConfigPage extends DeployConfigPage {
}
SetDatabaseDropDown(): void {
this.databaseDropdown.value = 'DummyDatabase';
this.databaseDropdown.value = { name: 'DummyDatabase', displayName: 'DummyDatabase' };
}
SetFileName(): void {

View File

@@ -104,12 +104,12 @@ export abstract class DacFxConfigPage extends BasePage {
// Handle database changes
this.databaseDropdown.onValueChanged(() => {
const databaseDropdownValue: string = this.databaseDropdown.value as string;
const databaseDropdownValue = this.databaseDropdown.value;
if (!databaseDropdownValue) {
return;
}
this.model.database = databaseDropdownValue;
this.model.database = databaseDropdownValue as string;
this.fileTextBox.value = this.generateFilePathFromDatabaseAndTimestamp();
this.model.filePath = this.fileTextBox.value;
});

View File

@@ -125,16 +125,7 @@ export class DataTierApplicationWizard {
this.setPages();
this.configureButtons();
// the wizard was started from the context menu of a database or server if the connectionProfile is not undefined
// Otherwise it was launched from the command palette
let launchedFrom: string;
if (profile) {
launchedFrom = profile.databaseName ? 'database context menu' : 'server context menu';
} else {
launchedFrom = 'command palette';
}
this.wizard.open(launchedFrom);
this.wizard.open();
return true;
}

View File

@@ -142,12 +142,12 @@ export class DeployConfigPage extends DacFxConfigPage {
//Handle database changes
this.databaseDropdown.onValueChanged(() => {
const databaseDropdownValue: string = this.databaseDropdown.value as string;
const databaseDropdownValue = this.databaseDropdown.value as azdata.CategoryValue;
if (!databaseDropdownValue) {
return;
}
this.model.database = databaseDropdownValue;
this.model.database = databaseDropdownValue.name;
});
this.databaseLoader = this.view.modelBuilder.loadingComponent().withItem(this.databaseDropdown).withProperties({

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.1328 0.296875C10.9974 0.53125 11.7891 0.898438 12.5078 1.39844C13.2266 1.89323 13.8438 2.48177 14.3594 3.16406C14.8802 3.84115 15.2839 4.59375 15.5703 5.42188C15.8568 6.24479 16 7.10417 16 8C16 8.73438 15.9036 9.44271 15.7109 10.125C15.5234 10.8073 15.2552 11.4453 14.9062 12.0391C14.5625 12.6328 14.1458 13.1745 13.6562 13.6641C13.1719 14.1484 12.6328 14.5651 12.0391 14.9141C11.4453 15.2578 10.8073 15.526 10.125 15.7188C9.44271 15.9062 8.73438 16 8 16C7.26562 16 6.55729 15.9062 5.875 15.7188C5.19271 15.526 4.55469 15.2578 3.96094 14.9141C3.36719 14.5651 2.82552 14.1484 2.33594 13.6641C1.85156 13.1745 1.4349 12.6328 1.08594 12.0391C0.742188 11.4453 0.473958 10.8099 0.28125 10.1328C0.09375 9.45052 0 8.73958 0 8C0 7.27083 0.0963542 6.5625 0.289062 5.875C0.481771 5.1875 0.755208 4.54167 1.10938 3.9375C1.46875 3.32812 1.90365 2.77604 2.41406 2.28125C2.92448 1.78125 3.5 1.35417 4.14062 1H2V0H6V4H5V1.67969C4.39062 1.97135 3.83854 2.33854 3.34375 2.78125C2.85417 3.21875 2.4349 3.71354 2.08594 4.26562C1.73698 4.8125 1.46875 5.40365 1.28125 6.03906C1.09375 6.67448 1 7.32812 1 8C1 8.64062 1.08333 9.26042 1.25 9.85938C1.41667 10.4531 1.65104 11.0104 1.95312 11.5312C2.26042 12.0469 2.6276 12.5182 3.05469 12.9453C3.48177 13.3724 3.95312 13.7396 4.46875 14.0469C4.98958 14.349 5.54688 14.5833 6.14062 14.75C6.73438 14.9167 7.35417 15 8 15C8.64062 15 9.25781 14.9167 9.85156 14.75C10.4505 14.5833 11.0078 14.349 11.5234 14.0469C12.0443 13.7396 12.5182 13.3724 12.9453 12.9453C13.3724 12.5182 13.737 12.0469 14.0391 11.5312C14.3464 11.0104 14.5833 10.4531 14.75 9.85938C14.9167 9.26562 15 8.64583 15 8C15 7.21875 14.8724 6.46615 14.6172 5.74219C14.3672 5.01823 14.0156 4.35938 13.5625 3.76562C13.1094 3.17188 12.5677 2.65885 11.9375 2.22656C11.3125 1.78906 10.6224 1.46615 9.86719 1.25781L10.1328 0.296875Z" fill="#0078D4"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -71,10 +71,6 @@
{
"command": "projects.removeProject",
"title": "%remove-project-command%"
},
{
"command": "projects.manageProject",
"title": "%manage-project-command%"
}
],
"menus": {
@@ -118,18 +114,9 @@
},
{
"command": "projects.openExisting"
},
{
"command": "projects.manageProject",
"when": "false"
}
],
"view/item/context": [
{
"command": "projects.manageProject",
"when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.project",
"group": "0_projectsFirst@1"
},
{
"command": "projects.removeProject",
"when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.project",

Some files were not shown because too many files have changed in this diff Show More