mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 11:01:37 -05:00
Compare commits
345 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13e3627627 | ||
|
|
e2111fe493 | ||
|
|
7b46269b44 | ||
|
|
69b9a19634 | ||
|
|
ebe835ec99 | ||
|
|
d4e25f4d89 | ||
|
|
809d4de862 | ||
|
|
e1aae951a3 | ||
|
|
bdd99bd0d8 | ||
|
|
c06bfad916 | ||
|
|
1c91f7971e | ||
|
|
8c9037fbdf | ||
|
|
d029bb9602 | ||
|
|
f0558714a4 | ||
|
|
49147305e8 | ||
|
|
519012c690 | ||
|
|
ff05bc2b03 | ||
|
|
b54beb6e7a | ||
|
|
dbdcc3d20a | ||
|
|
cf48776710 | ||
|
|
309f750b92 | ||
|
|
8c92af3016 | ||
|
|
9b117da9cb | ||
|
|
d12a7b81fd | ||
|
|
3eb705e77b | ||
|
|
b421b19b73 | ||
|
|
124f7ca887 | ||
|
|
c2d2cf5a82 | ||
|
|
13ad4c9497 | ||
|
|
684dfc9760 | ||
|
|
fde5caa9a4 | ||
|
|
0d3112ef35 | ||
|
|
e7be1daf5c | ||
|
|
30a2b76faf | ||
|
|
ce6ea8af41 | ||
|
|
a00ffa0469 | ||
|
|
37af88e265 | ||
|
|
4c2969d4ca | ||
|
|
97ce7b9b67 | ||
|
|
ef8396f783 | ||
|
|
fc13fb2200 | ||
|
|
2164bc0bb9 | ||
|
|
5151f337bc | ||
|
|
6ab9844909 | ||
|
|
d6e0a679f7 | ||
|
|
eb5405d116 | ||
|
|
0fa9689345 | ||
|
|
1ce10cd3f3 | ||
|
|
f7b9ca775a | ||
|
|
e762f19815 | ||
|
|
00d2fadb7d | ||
|
|
cb619f55c7 | ||
|
|
28577baa87 | ||
|
|
8aa222d1c8 | ||
|
|
b774f09b6c | ||
|
|
4df77c73bf | ||
|
|
f4e1f85e0f | ||
|
|
af4ad1fcb1 | ||
|
|
d3f9277139 | ||
|
|
7dfafe1393 | ||
|
|
a13e924a14 | ||
|
|
a231d2aa82 | ||
|
|
69361b5c97 | ||
|
|
896d0b0ea1 | ||
|
|
bdbe4fb6de | ||
|
|
4c5a4215f8 | ||
|
|
dd5adad772 | ||
|
|
ca19f08582 | ||
|
|
bbee4a1f38 | ||
|
|
39a47b0053 | ||
|
|
e080770c19 | ||
|
|
4d78aefe57 | ||
|
|
e0f24cc268 | ||
|
|
4b5aac57d3 | ||
|
|
5317d9ae0b | ||
|
|
b7e982e78a | ||
|
|
15f7b12849 | ||
|
|
94c0795fc7 | ||
|
|
5db6857c49 | ||
|
|
edac96a624 | ||
|
|
f61288d29e | ||
|
|
f12c8cd5d3 | ||
|
|
2b7535f377 | ||
|
|
f59e9b5695 | ||
|
|
130a439abc | ||
|
|
dee5720676 | ||
|
|
92e9a423a0 | ||
|
|
0fb01b5b34 | ||
|
|
b148b91a9c | ||
|
|
689144a526 | ||
|
|
a3681dc0f9 | ||
|
|
4f255647ac | ||
|
|
e6a81d01cc | ||
|
|
7d94e247f6 | ||
|
|
de494f97ef | ||
|
|
be9d18428f | ||
|
|
6c54059f89 | ||
|
|
5c67f3dbed | ||
|
|
e2b6972ba9 | ||
|
|
f125e014fa | ||
|
|
8d0131b624 | ||
|
|
2d43cca3c8 | ||
|
|
0d951e1ef1 | ||
|
|
339d908d1d | ||
|
|
780ca84f9a | ||
|
|
8fb54710fb | ||
|
|
1a74d0f3d4 | ||
|
|
72295d46c2 | ||
|
|
98ba49304e | ||
|
|
6ee321a719 | ||
|
|
45fd89d6da | ||
|
|
128d382c91 | ||
|
|
2086ca6039 | ||
|
|
a5d2870344 | ||
|
|
d658af153d | ||
|
|
ce39b4bd19 | ||
|
|
69d593007e | ||
|
|
8aa8cb2cc6 | ||
|
|
adc0f3e96d | ||
|
|
8068de5938 | ||
|
|
c65c856d2f | ||
|
|
31ce58a8fc | ||
|
|
af19f93b78 | ||
|
|
172f428789 | ||
|
|
a0a97d1611 | ||
|
|
11d4ea3232 | ||
|
|
5da4690be8 | ||
|
|
88b7960d01 | ||
|
|
03d2ac7206 | ||
|
|
07bd534453 | ||
|
|
947633f244 | ||
|
|
de91969b14 | ||
|
|
c7cca7e9f0 | ||
|
|
5917f869ef | ||
|
|
f4d305ec16 | ||
|
|
3d78fe02a4 | ||
|
|
04d9b54c49 | ||
|
|
a12f85dd02 | ||
|
|
7ffa7a9b23 | ||
|
|
4425b2afd3 | ||
|
|
a4fae7c1d8 | ||
|
|
150dec7211 | ||
|
|
89e5ee3c80 | ||
|
|
784d76b886 | ||
|
|
ff766a8a14 | ||
|
|
95fa61ee4e | ||
|
|
0ecc053377 | ||
|
|
97813b4c20 | ||
|
|
318559dcd7 | ||
|
|
7a4852b047 | ||
|
|
9beb7804e6 | ||
|
|
0209b55ea2 | ||
|
|
05d88f4971 | ||
|
|
4140d0da79 | ||
|
|
b3e141a150 | ||
|
|
9df7c6c1aa | ||
|
|
7a9de9e708 | ||
|
|
42347b4681 | ||
|
|
63828dcc15 | ||
|
|
98828aa67d | ||
|
|
d6cd3270a8 | ||
|
|
90a5d9d817 | ||
|
|
138fa3feb6 | ||
|
|
7ea09d3d17 | ||
|
|
8acc28e344 | ||
|
|
f83925ce38 | ||
|
|
b670ffc7e6 | ||
|
|
0277054b1f | ||
|
|
1c671676bf | ||
|
|
063953f743 | ||
|
|
bba3b93c6e | ||
|
|
cf862854c5 | ||
|
|
6c877a4e20 | ||
|
|
29c02e5746 | ||
|
|
50ac3b0fdf | ||
|
|
46f805a8be | ||
|
|
e97429cb74 | ||
|
|
03a0d71486 | ||
|
|
ee2988f5fd | ||
|
|
6389a5b0b0 | ||
|
|
bfa8ec0301 | ||
|
|
c05ba883c4 | ||
|
|
c33116c625 | ||
|
|
973f151e07 | ||
|
|
4b462a41ab | ||
|
|
0906030aa2 | ||
|
|
46dd49d69a | ||
|
|
a4297a2922 | ||
|
|
447e573911 | ||
|
|
b5e66a715f | ||
|
|
e2a5859155 | ||
|
|
bbdc324f17 | ||
|
|
0bea923c0a | ||
|
|
809d35f173 | ||
|
|
16556b8316 | ||
|
|
9d1db372f0 | ||
|
|
dba5880f35 | ||
|
|
cecc83c89d | ||
|
|
972b649beb | ||
|
|
0ef99ab42a | ||
|
|
f125b9b2c7 | ||
|
|
733c3628a1 | ||
|
|
1d60287795 | ||
|
|
63c59ed920 | ||
|
|
df06afa2ab | ||
|
|
89c3207c94 | ||
|
|
a17a4a585e | ||
|
|
b82942a030 | ||
|
|
f6f45bc3f9 | ||
|
|
d2faf9075d | ||
|
|
21019f7452 | ||
|
|
1167e247af | ||
|
|
eef8f0e2ba | ||
|
|
4e1c7decde | ||
|
|
8e07a6f239 | ||
|
|
99d46917e8 | ||
|
|
9cfba8e8e0 | ||
|
|
69a35b38b2 | ||
|
|
0141db80bc | ||
|
|
ccde5123fa | ||
|
|
9c886dd80a | ||
|
|
8c04266ff4 | ||
|
|
8e2fd2ac84 | ||
|
|
74629c951d | ||
|
|
2620d060dc | ||
|
|
42fba14d88 | ||
|
|
6ecacd6faa | ||
|
|
ae55da3c35 | ||
|
|
cea55560e8 | ||
|
|
625c4520da | ||
|
|
56dcd4ba50 | ||
|
|
3f0ca8b714 | ||
|
|
36e228ebf7 | ||
|
|
7940714d00 | ||
|
|
f2ae5419bb | ||
|
|
1e67388653 | ||
|
|
18bdb0f37d | ||
|
|
3c38e0cc8b | ||
|
|
8b34e034fc | ||
|
|
9e02123330 | ||
|
|
cf08963fc1 | ||
|
|
7d53e1c185 | ||
|
|
9273572d5a | ||
|
|
ff508b1bf4 | ||
|
|
af3373bc4b | ||
|
|
14b9628b52 | ||
|
|
9432330c46 | ||
|
|
48a63e1f50 | ||
|
|
8ce32215ba | ||
|
|
5d07a3272e | ||
|
|
ad045de1f0 | ||
|
|
ac9a2dcf68 | ||
|
|
b10626d6f4 | ||
|
|
c6c65000ca | ||
|
|
862d0d88fa | ||
|
|
b304fadfde | ||
|
|
a378a52b33 | ||
|
|
013ecc4ddc | ||
|
|
ab114376aa | ||
|
|
dbc655a8f5 | ||
|
|
e761eb12ef | ||
|
|
8db8027b5a | ||
|
|
9e02cf86a4 | ||
|
|
4053666bef | ||
|
|
2a125ee43b | ||
|
|
b28e845506 | ||
|
|
dbb6b71908 | ||
|
|
afa2256467 | ||
|
|
f461d2aa14 | ||
|
|
00feb955d9 | ||
|
|
71cbe3fbf0 | ||
|
|
3d5ff25d13 | ||
|
|
48c456709e | ||
|
|
4b40d66bca | ||
|
|
587ac45418 | ||
|
|
9d827869a1 | ||
|
|
94f7b329d6 | ||
|
|
c05cece683 | ||
|
|
d5385f66d3 | ||
|
|
0108da2a24 | ||
|
|
561242a0d9 | ||
|
|
948bb5bc34 | ||
|
|
e266bbd562 | ||
|
|
eb58cd74e7 | ||
|
|
2e1fe9a266 | ||
|
|
9148aa1ed5 | ||
|
|
d21ee4dc9e | ||
|
|
9daaa1c58b | ||
|
|
5ece9b968a | ||
|
|
551eb76a42 | ||
|
|
f62889002d | ||
|
|
9087a9fbb1 | ||
|
|
cd4024625f | ||
|
|
138d3f97ba | ||
|
|
18b93cf212 | ||
|
|
289dd3ba65 | ||
|
|
6ca777ad31 | ||
|
|
83da03a728 | ||
|
|
f528ffea9b | ||
|
|
eb61af2f12 | ||
|
|
8727a8e97a | ||
|
|
fcd099dd64 | ||
|
|
ce18341284 | ||
|
|
aff9adf730 | ||
|
|
b719099ad3 | ||
|
|
30f55be67d | ||
|
|
87a9ae0975 | ||
|
|
272c20bb38 | ||
|
|
d159a1eb50 | ||
|
|
05f97411fa | ||
|
|
56a6557c6e | ||
|
|
c84e092473 | ||
|
|
21fae18a13 | ||
|
|
0c1b16d4fb | ||
|
|
f4cf506290 | ||
|
|
54facabde3 | ||
|
|
0f748a60b3 | ||
|
|
238a0c60d9 | ||
|
|
415689de9f | ||
|
|
612c385725 | ||
|
|
6a09d8387e | ||
|
|
35c8a20eb3 | ||
|
|
c82e80e61f | ||
|
|
6eba54d819 | ||
|
|
56d2f6b497 | ||
|
|
c456b81071 | ||
|
|
767a197780 | ||
|
|
692336e6a7 | ||
|
|
e8d02dbc44 | ||
|
|
75cda19504 | ||
|
|
b5479d0246 | ||
|
|
7739f25f7f | ||
|
|
756454efa9 | ||
|
|
c511d1a78f | ||
|
|
5e168b0960 | ||
|
|
d8438bd720 | ||
|
|
49a989ac14 | ||
|
|
e4c5af2663 | ||
|
|
6aafc252e6 | ||
|
|
e65a4ac13d | ||
|
|
45adb7d8d7 | ||
|
|
bb29ae00c9 | ||
|
|
800cd2fa89 | ||
|
|
61f9da39fa | ||
|
|
ce69ecebc4 |
106
.github/workflows/ci.yml
vendored
106
.github/workflows/ci.yml
vendored
@@ -11,57 +11,57 @@ on:
|
||||
- release/*
|
||||
|
||||
jobs:
|
||||
# linux:
|
||||
# runs-on: ubuntu-latest
|
||||
# env:
|
||||
# CHILD_CONCURRENCY: "1"
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2.2.0
|
||||
# # TODO: rename azure-pipelines/linux/xvfb.init to github-actions
|
||||
# - run: |
|
||||
# sudo apt-get update
|
||||
# sudo apt-get install -y libxkbfile-dev pkg-config libsecret-1-dev libxss1 dbus xvfb libgtk-3-0 libkrb5-dev # {{SQL CARBON EDIT}} add kerberos dep
|
||||
# sudo cp build/azure-pipelines/linux/xvfb.init /etc/init.d/xvfb
|
||||
# sudo chmod +x /etc/init.d/xvfb
|
||||
# sudo update-rc.d xvfb defaults
|
||||
# sudo service xvfb start
|
||||
# name: Setup Build Environment
|
||||
# - uses: actions/setup-node@v1
|
||||
# with:
|
||||
# node-version: 10
|
||||
# # TODO: cache node modules
|
||||
# # Increase timeout to get around latency issues when fetching certain packages
|
||||
# - run: |
|
||||
# yarn config set network-timeout 300000
|
||||
# yarn --frozen-lockfile
|
||||
# name: Install Dependencies
|
||||
# - run: yarn electron x64
|
||||
# name: Download Electron
|
||||
# - run: yarn gulp hygiene
|
||||
# name: Run Hygiene Checks
|
||||
# - run: yarn strict-vscode # {{SQL CARBON EDIT}} add step
|
||||
# name: Run Strict Compile Options
|
||||
# # - run: yarn monaco-compile-check {{SQL CARBON EDIT}} remove step
|
||||
# # name: Run Monaco Editor Checks
|
||||
# - run: yarn valid-layers-check
|
||||
# name: Run Valid Layers Checks
|
||||
# - run: yarn compile
|
||||
# name: Compile Sources
|
||||
# # - run: yarn download-builtin-extensions {{SQL CARBON EDIT}} remove step
|
||||
# # name: Download Built-in Extensions
|
||||
# - run: DISPLAY=:10 ./scripts/test.sh --tfs "Unit Tests" --coverage --runGlob "**/sql/**/*.test.js"
|
||||
# name: Run Unit Tests (Electron)
|
||||
# - run: DISPLAY=:10 ./scripts/test-extensions-unit.sh
|
||||
# name: Run Extension Unit Tests (Electron)
|
||||
# # {{SQL CARBON EDIT}} Add coveralls. We merge first to get around issue where parallel builds weren't being combined correctly
|
||||
# - run: node test/combineCoverage
|
||||
# name: Combine code coverage files
|
||||
# - name: Upload Code Coverage
|
||||
# uses: coverallsapp/github-action@v1.1.1
|
||||
# with:
|
||||
# github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# path-to-lcov: "test/coverage/lcov.info"
|
||||
linux:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CHILD_CONCURRENCY: "1"
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2.2.0
|
||||
# TODO: rename azure-pipelines/linux/xvfb.init to github-actions
|
||||
- run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxkbfile-dev pkg-config libsecret-1-dev libxss1 dbus xvfb libgtk-3-0 libkrb5-dev # {{SQL CARBON EDIT}} add kerberos dep
|
||||
sudo cp build/azure-pipelines/linux/xvfb.init /etc/init.d/xvfb
|
||||
sudo chmod +x /etc/init.d/xvfb
|
||||
sudo update-rc.d xvfb defaults
|
||||
sudo service xvfb start
|
||||
name: Setup Build Environment
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
# TODO: cache node modules
|
||||
# Increase timeout to get around latency issues when fetching certain packages
|
||||
- run: |
|
||||
yarn config set network-timeout 300000
|
||||
yarn --frozen-lockfile
|
||||
name: Install Dependencies
|
||||
- run: yarn electron x64
|
||||
name: Download Electron
|
||||
- run: yarn gulp hygiene
|
||||
name: Run Hygiene Checks
|
||||
- run: yarn strict-vscode # {{SQL CARBON EDIT}} add step
|
||||
name: Run Strict Compile Options
|
||||
# - run: yarn monaco-compile-check {{SQL CARBON EDIT}} remove step
|
||||
# name: Run Monaco Editor Checks
|
||||
- run: yarn valid-layers-check
|
||||
name: Run Valid Layers Checks
|
||||
- run: yarn compile
|
||||
name: Compile Sources
|
||||
# - run: yarn download-builtin-extensions {{SQL CARBON EDIT}} remove step
|
||||
# name: Download Built-in Extensions
|
||||
- run: DISPLAY=:10 ./scripts/test.sh --tfs "Unit Tests" --coverage --runGlob "**/sql/**/*.test.js"
|
||||
name: Run Unit Tests (Electron)
|
||||
- run: DISPLAY=:10 ./scripts/test-extensions-unit.sh
|
||||
name: Run Extension Unit Tests (Electron)
|
||||
# {{SQL CARBON EDIT}} Add coveralls. We merge first to get around issue where parallel builds weren't being combined correctly
|
||||
- run: node test/combineCoverage
|
||||
name: Combine code coverage files
|
||||
- name: Upload Code Coverage
|
||||
uses: coverallsapp/github-action@v1.1.1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
path-to-lcov: "test/coverage/lcov.info"
|
||||
|
||||
# Fails with cryptic error (e.g. https://github.com/microsoft/vscode/pull/90292/checks?check_run_id=433681926#step:13:9)
|
||||
# - run: DISPLAY=:10 yarn test-browser --browser chromium
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
- uses: actions/checkout@v2.2.0
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10
|
||||
node-version: 12
|
||||
- uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: "2.x"
|
||||
@@ -117,7 +117,7 @@ jobs:
|
||||
- uses: actions/checkout@v2.2.0
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10
|
||||
node-version: 12
|
||||
# Increase timeout to get around latency issues when fetching certain packages
|
||||
- run: |
|
||||
yarn config set network-timeout 300000
|
||||
|
||||
45
CHANGELOG.md
45
CHANGELOG.md
@@ -1,5 +1,50 @@
|
||||
# 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
|
||||
* Fixes https://github.com/microsoft/azuredatastudio/issues/14382
|
||||
|
||||
## Version 1.26.0
|
||||
* Release date: February 22, 2021
|
||||
* Release status: General Availability
|
||||
* Added edit Jupyter book UI support
|
||||
* Improved Jupyter server start-up time by 50% on windows
|
||||
* Extension Updates:
|
||||
* Azure Arc
|
||||
* PG dashboard enhancements
|
||||
* Multi-controller support
|
||||
* MIAA Dashboard will no longer prompt for SQL Server connection immediately upon opening
|
||||
* Azure Data CLI
|
||||
* Kusto
|
||||
* Machine Learning
|
||||
* Profiler
|
||||
* Server Reports
|
||||
* Schema Compare
|
||||
* SQL Server Dacpac
|
||||
* SQL Database Projects
|
||||
* Bug Fixes
|
||||
|
||||
## Version 1.25.3
|
||||
* Release date: February 10, 2021
|
||||
* Release status: General Availability
|
||||
* Update Electron to 9.4.3 to incorporate critical upstream fixes
|
||||
|
||||
## Version 1.25.2
|
||||
* Release date: January 22, 2021
|
||||
* Release status: General Availability
|
||||
|
||||
14
README.md
14
README.md
@@ -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=2150927
|
||||
[win-system]: https://go.microsoft.com/fwlink/?linkid=2150928
|
||||
[win-zip]: https://go.microsoft.com/fwlink/?linkid=2151312
|
||||
[osx-zip]: https://go.microsoft.com/fwlink/?linkid=2151311
|
||||
[linux-zip]: https://go.microsoft.com/fwlink/?linkid=2151508
|
||||
[linux-rpm]: https://go.microsoft.com/fwlink/?linkid=2151407
|
||||
[linux-deb]: https://go.microsoft.com/fwlink/?linkid=2151506
|
||||
[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
|
||||
|
||||
@@ -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,7 +54,8 @@ 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
|
||||
qs: https://github.com/ljharb/qs
|
||||
pyzmq: https://github.com/zeromq/pyzmq
|
||||
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
|
||||
@@ -1548,6 +1549,40 @@ 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
|
||||
|
||||
@@ -122,14 +122,14 @@ steps:
|
||||
displayName: Run integration tests (Electron)
|
||||
condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true'))
|
||||
|
||||
# - script: |
|
||||
# set -e
|
||||
# APP_ROOT=$(agent.builddirectory)/azuredatastudio-darwin-x64
|
||||
# APP_NAME="`ls $APP_ROOT | head -n 1`"
|
||||
# yarn smoketest --build "$APP_ROOT/$APP_NAME" --screenshots "$(build.artifactstagingdirectory)/smokeshots" --log "$(build.artifactstagingdirectory)/logs/darwin/smoke.log"
|
||||
# displayName: Run smoke tests (Electron)
|
||||
# continueOnError: true
|
||||
# condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true'))
|
||||
- script: |
|
||||
set -e
|
||||
APP_ROOT=$(agent.builddirectory)/azuredatastudio-darwin-x64
|
||||
APP_NAME="`ls $APP_ROOT | head -n 1`"
|
||||
yarn smoketest --build "$APP_ROOT/$APP_NAME" --screenshots "$(build.artifactstagingdirectory)/smokeshots" --log "$(build.artifactstagingdirectory)/logs/darwin/smoke.log"
|
||||
displayName: Run smoke tests (Electron)
|
||||
continueOnError: true
|
||||
condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true'))
|
||||
|
||||
# - script: |
|
||||
# set -e
|
||||
|
||||
@@ -46,8 +46,7 @@ jobs:
|
||||
steps:
|
||||
- template: linux/sql-product-build-linux.yml
|
||||
parameters:
|
||||
# extensionsToUnitTest: ["admin-tool-ext-win", "agent", "azdata", "azurecore", "cms", "dacpac", "import", "schema-compare", "notebook", "resource-deployment", "machine-learning", "sql-database-projects", "data-workspace"]
|
||||
extensionsToUnitTest: ["admin-tool-ext-win", "agent", "azdata", "azurecore", "cms", "dacpac", "import", "schema-compare", "resource-deployment", "machine-learning", "sql-database-projects", "data-workspace"]
|
||||
extensionsToUnitTest: ["admin-tool-ext-win", "agent", "azdata", "azurecore", "cms", "dacpac", "import", "schema-compare", "notebook", "resource-deployment", "machine-learning", "sql-database-projects", "data-workspace"]
|
||||
timeoutInMinutes: 70
|
||||
|
||||
- job: LinuxWeb
|
||||
|
||||
@@ -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,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,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'
|
||||
signConfigType: inlineSignParams
|
||||
inlineOperation: |
|
||||
[
|
||||
|
||||
@@ -30,7 +30,7 @@ const all = [
|
||||
'test/**/*',
|
||||
'!test/**/out/**',
|
||||
'!**/node_modules/**',
|
||||
'!build/actions/**/*.js', // {{ SQL CARBON EDIT }}
|
||||
'!build/actions/**/*.js', // {{SQL CARBON EDIT}}
|
||||
'!build/**/*' // {{SQL CARBON EDIT}}
|
||||
];
|
||||
module.exports.all = all;
|
||||
@@ -114,6 +114,9 @@ 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}}
|
||||
];
|
||||
|
||||
|
||||
@@ -3235,9 +3235,9 @@ string_decoder@~0.10.x:
|
||||
integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=
|
||||
|
||||
stringstream@~0.0.4, stringstream@~0.0.5:
|
||||
version "0.0.5"
|
||||
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
|
||||
integrity sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.6.tgz#7880225b0d4ad10e30927d167a1d6f2fd3b33a72"
|
||||
integrity sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==
|
||||
|
||||
strip-ansi@^3.0.0:
|
||||
version "3.0.1"
|
||||
@@ -3751,9 +3751,9 @@ xtend@~4.0.1:
|
||||
integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68=
|
||||
|
||||
y18n@^4.0.0:
|
||||
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==
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4"
|
||||
integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==
|
||||
|
||||
yallist@^4.0.0:
|
||||
version "4.0.0"
|
||||
|
||||
@@ -60,12 +60,12 @@
|
||||
"git": {
|
||||
"name": "electron",
|
||||
"repositoryUrl": "https://github.com/electron/electron",
|
||||
"commitHash": "415c1f9e9b35d9599b1a8ad1200476afa47a3323"
|
||||
"commitHash": "ca82414364002efa665ffa7427e267adf76ed1f3"
|
||||
}
|
||||
},
|
||||
"isOnlyProductionDependency": true,
|
||||
"license": "MIT",
|
||||
"version": "9.3.5"
|
||||
"version": "9.4.3"
|
||||
},
|
||||
{
|
||||
"component": {
|
||||
|
||||
17
extensions/arc/images/gear-colored-blue.svg
Normal file
17
extensions/arc/images/gear-colored-blue.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 18 18" style="enable-background:new 0 0 18 18;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:url(#SVGID_1_);}
|
||||
</style>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="9" y1="2" x2="9" y2="20" gradientTransform="matrix(1 0 0 -1 0 20)">
|
||||
<stop offset="0" style="stop-color:#32BEDD"/>
|
||||
<stop offset="0.576" style="stop-color:#32CEEF"/>
|
||||
<stop offset="1" style="stop-color:#32D4F5"/>
|
||||
</linearGradient>
|
||||
<path class="st0" d="M18,10V7.9l-0.3-0.1l-2.2-0.7l-0.6-1.4l1.1-2.4l-1.4-1.4L14.3,2l-2,1l-1.4-0.6L10,0H7.9L7.8,0.3L7.1,2.5
|
||||
L5.7,3.1L3.3,1.9L1.9,3.4l0.1,0.3l1,2.1L2.5,7.1L0,8v2.1l0.3,0.1l2.2,0.7l0.6,1.4l-1.1,2.4l1.4,1.4L3.7,16l2.1-1l1.4,0.6L8,18h2.1
|
||||
l0.1-0.3l0.7-2.2l1.4-0.6l2.4,1.1l1.4-1.4L16,14.3l-1-2l0.6-1.4L18,10z M9,12.9c-2.2,0-4-1.8-4-3.9s1.8-4,4-4s3.9,1.8,3.9,4l0,0
|
||||
C13,11.2,11.2,12.9,9,12.9C9,13,9,13,9,12.9z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
16
extensions/arc/images/gear-colored-gray.svg
Normal file
16
extensions/arc/images/gear-colored-gray.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 18 18" style="enable-background:new 0 0 18 18;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:url(#SVGID_1_);}
|
||||
</style>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="9" y1="2" x2="9" y2="20" gradientTransform="matrix(1 0 0 -1 0 20)">
|
||||
<stop offset="1.000000e-03" style="stop-color:#767676"/>
|
||||
<stop offset="1" style="stop-color:#D9D9D9"/>
|
||||
</linearGradient>
|
||||
<path class="st0" d="M18,10V7.9l-0.3-0.1l-2.2-0.7l-0.6-1.4l1.1-2.4l-1.4-1.4L14.3,2l-2,1l-1.4-0.6L10,0H7.9L7.8,0.3L7.1,2.5
|
||||
L5.7,3.1L3.3,1.9L1.9,3.4l0.1,0.3l1,2.1L2.5,7.1L0,8v2.1l0.3,0.1l2.2,0.7l0.6,1.4l-1.1,2.4l1.4,1.4L3.7,16l2.1-1l1.4,0.6L8,18h2.1
|
||||
l0.1-0.3l0.7-2.2l1.4-0.6l2.4,1.1l1.4-1.4L16,14.3l-1-2l0.6-1.4L18,10z M9,12.9c-2.2,0-4-1.8-4-3.9s1.8-4,4-4s3.9,1.8,3.9,4l0,0
|
||||
C13,11.2,11.2,12.9,9,12.9C9,13,9,13,9,12.9z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
3
extensions/arc/images/gear-monoline-black.svg
Normal file
3
extensions/arc/images/gear-monoline-black.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path d="M13.95,7.75h0v.3h0l2,1.3-1.2,3-2.4-.3-.3.3.5,2.4-3,1.2-1.3-2h-.5l-1.3,2-3-1.2.6-2.4-.4-.3-2.4.5-1.2-3,2-1.3h0v-.4h0l-2-1.4,1.2-3,2.4.6a.8.8,0,0,1,.4-.4l-.6-2.4,3-1.2,1.3,2h.5l1.3-2,3,1.2-.5,2.4.3.4,2.4-.6,1.2,3Zm-.9,1V7.25l1.8-1.1-.7-1.7-2.1.5a1.205,1.205,0,0,0-.5-.5,1.205,1.205,0,0,0-.5-.5l.5-2.1-1.7-.7-1.1,1.8H7.25l-1.1-1.8-1.7.7.5,2.1a1.205,1.205,0,0,0-.5.5,1.205,1.205,0,0,0-.5.5l-2.1-.5-.7,1.7,1.8,1.1v1.5l-1.8,1.1.7,1.7,2.1-.5.5.5.5.5-.5,2.1,1.7.7,1.1-1.8h1.5l1.1,1.8,1.7-.7-.5-2.1.5-.5.5-.5,2.1.5.7-1.7Zm-5-3.7,1.1.2a2.2,2.2,0,0,1,.9.7,3.1,3.1,0,0,1,.7.9,4.3,4.3,0,0,1,.2,1.2,4.328,4.328,0,0,1-.2,1.1,2.2,2.2,0,0,1-.7.9,2.2,2.2,0,0,1-.9.7l-1.1.2-1.2-.2a3.1,3.1,0,0,1-.9-.7,2.2,2.2,0,0,1-.7-.9,4.328,4.328,0,0,1-.2-1.1,4.3,4.3,0,0,1,.2-1.2,3.1,3.1,0,0,1,.7-.9,3.1,3.1,0,0,1,.9-.7Zm0,5h.8l.6-.5.5-.6a2.489,2.489,0,0,0,.1-.9,2.793,2.793,0,0,0-.1-.9l-.5-.6-.6-.4a1.3,1.3,0,0,0-.8-.2,1.507,1.507,0,0,0-.9.2l-.6.4-.4.6a1.507,1.507,0,0,0-.2.9,1.3,1.3,0,0,0,.2.8l.4.6.6.5Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
3
extensions/arc/images/gear-monoline-blue.svg
Normal file
3
extensions/arc/images/gear-monoline-blue.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path d="M13.95,7.75h0v.3h0l2,1.3-1.2,3-2.4-.3-.3.3.5,2.4-3,1.2-1.3-2h-.5l-1.3,2-3-1.2.6-2.4-.4-.3-2.4.5-1.2-3,2-1.3h0v-.4h0l-2-1.4,1.2-3,2.4.6a.8.8,0,0,1,.4-.4l-.6-2.4,3-1.2,1.3,2h.5l1.3-2,3,1.2-.5,2.4.3.4,2.4-.6,1.2,3Zm-.9,1V7.25l1.8-1.1-.7-1.7-2.1.5a1.205,1.205,0,0,0-.5-.5,1.205,1.205,0,0,0-.5-.5l.5-2.1-1.7-.7-1.1,1.8H7.25l-1.1-1.8-1.7.7.5,2.1a1.205,1.205,0,0,0-.5.5,1.205,1.205,0,0,0-.5.5l-2.1-.5-.7,1.7,1.8,1.1v1.5l-1.8,1.1.7,1.7,2.1-.5.5.5.5.5-.5,2.1,1.7.7,1.1-1.8h1.5l1.1,1.8,1.7-.7-.5-2.1.5-.5.5-.5,2.1.5.7-1.7Zm-5-3.7,1.1.2a2.2,2.2,0,0,1,.9.7,3.1,3.1,0,0,1,.7.9,4.3,4.3,0,0,1,.2,1.2,4.328,4.328,0,0,1-.2,1.1,2.2,2.2,0,0,1-.7.9,2.2,2.2,0,0,1-.9.7l-1.1.2-1.2-.2a3.1,3.1,0,0,1-.9-.7,2.2,2.2,0,0,1-.7-.9,4.328,4.328,0,0,1-.2-1.1,4.3,4.3,0,0,1,.2-1.2,3.1,3.1,0,0,1,.7-.9,3.1,3.1,0,0,1,.9-.7Zm0,5h.8l.6-.5.5-.6a2.489,2.489,0,0,0,.1-.9,2.793,2.793,0,0,0-.1-.9l-.5-.6-.6-.4a1.3,1.3,0,0,0-.8-.2,1.507,1.507,0,0,0-.9.2l-.6.4-.4.6a1.507,1.507,0,0,0-.2.9,1.3,1.3,0,0,0,.2.8l.4.6.6.5Z" fill="#0078D4" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
3
extensions/arc/images/gear-monoline-white.svg
Normal file
3
extensions/arc/images/gear-monoline-white.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.9297 7.71875C13.9297 7.76562 13.9297 7.8125 13.9297 7.85938C13.9349 7.90625 13.9375 7.95312 13.9375 8C13.9375 8.04688 13.9349 8.09375 13.9297 8.14062C13.9297 8.1875 13.9297 8.23438 13.9297 8.28125L15.9531 9.53906L14.7109 12.5312L12.3906 12C12.2656 12.1354 12.1354 12.2656 12 12.3906L12.5312 14.7109L9.53906 15.9531L8.28125 13.9297C8.23438 13.9297 8.1875 13.9323 8.14062 13.9375C8.09375 13.9375 8.04688 13.9375 8 13.9375C7.95312 13.9375 7.90625 13.9375 7.85938 13.9375C7.8125 13.9323 7.76562 13.9297 7.71875 13.9297L6.46094 15.9531L3.46875 14.7109L4 12.3906C3.86458 12.2656 3.73438 12.1354 3.60938 12L1.28906 12.5312L0.046875 9.53906L2.07031 8.28125C2.07031 8.23438 2.06771 8.1875 2.0625 8.14062C2.0625 8.09375 2.0625 8.04688 2.0625 8C2.0625 7.95312 2.0625 7.90625 2.0625 7.85938C2.06771 7.8125 2.07031 7.76562 2.07031 7.71875L0.046875 6.46094L1.28906 3.46875L3.60938 4C3.73438 3.86458 3.86458 3.73438 4 3.60938L3.46875 1.28906L6.46094 0.046875L7.71875 2.07031C7.76562 2.07031 7.8125 2.07031 7.85938 2.07031C7.90625 2.0651 7.95312 2.0625 8 2.0625C8.04688 2.0625 8.09375 2.0651 8.14062 2.07031C8.1875 2.07031 8.23438 2.07031 8.28125 2.07031L9.53906 0.046875L12.5312 1.28906L12 3.60938C12.1354 3.73438 12.2656 3.86458 12.3906 4L14.7109 3.46875L15.9531 6.46094L13.9297 7.71875ZM13.0156 8.73438C13.026 8.60938 13.0365 8.48698 13.0469 8.36719C13.0573 8.24219 13.0625 8.11719 13.0625 7.99219C13.0625 7.8724 13.0573 7.75 13.0469 7.625C13.0365 7.5 13.026 7.3776 13.0156 7.25781L14.8594 6.10938L14.1875 4.48438L12.0703 4.97656C11.9089 4.77865 11.7422 4.59635 11.5703 4.42969C11.4036 4.26302 11.2214 4.09635 11.0234 3.92969L11.5156 1.8125L9.89062 1.14062L8.73438 2.98438C8.61458 2.97396 8.49219 2.96354 8.36719 2.95312C8.24219 2.94271 8.11979 2.9375 8 2.9375C7.875 2.9375 7.75 2.94271 7.625 2.95312C7.50521 2.96354 7.38281 2.97396 7.25781 2.98438L6.10938 1.14062L4.48438 1.8125L4.97656 3.92969C4.77865 4.09115 4.59635 4.25781 4.42969 4.42969C4.26302 4.59635 4.09635 4.77865 3.92969 4.97656L1.8125 4.48438L1.14062 6.10938L2.98438 7.26562C2.97396 7.39062 2.96354 7.51562 2.95312 7.64062C2.94271 7.76042 2.9375 7.88281 2.9375 8.00781C2.9375 8.1276 2.94271 8.25 2.95312 8.375C2.96354 8.5 2.97396 8.6224 2.98438 8.74219L1.14062 9.89062L1.8125 11.5156L3.92969 11.0234C4.09115 11.2214 4.25521 11.4036 4.42188 11.5703C4.59375 11.737 4.77865 11.9036 4.97656 12.0703L4.48438 14.1875L6.10938 14.8594L7.26562 13.0156C7.38542 13.026 7.50781 13.0365 7.63281 13.0469C7.75781 13.0573 7.88021 13.0625 8 13.0625C8.125 13.0625 8.2474 13.0573 8.36719 13.0469C8.49219 13.0365 8.61719 13.026 8.74219 13.0156L9.89062 14.8594L11.5156 14.1875L11.0234 12.0703C11.2214 11.9089 11.4036 11.7448 11.5703 11.5781C11.737 11.4062 11.9036 11.2214 12.0703 11.0234L14.1875 11.5156L14.8594 9.89062L13.0156 8.73438ZM8 5.0625C8.40625 5.0625 8.78646 5.14062 9.14062 5.29688C9.5 5.44792 9.8125 5.65625 10.0781 5.92188C10.3438 6.1875 10.5521 6.5 10.7031 6.85938C10.8594 7.21354 10.9375 7.59375 10.9375 8C10.9375 8.40625 10.8594 8.78906 10.7031 9.14844C10.5521 9.5026 10.3438 9.8125 10.0781 10.0781C9.8125 10.3438 9.5 10.5547 9.14062 10.7109C8.78646 10.862 8.40625 10.9375 8 10.9375C7.59375 10.9375 7.21094 10.862 6.85156 10.7109C6.4974 10.5547 6.1875 10.3438 5.92188 10.0781C5.65625 9.8125 5.44531 9.5026 5.28906 9.14844C5.13802 8.78906 5.0625 8.40625 5.0625 8C5.0625 7.59375 5.13802 7.21354 5.28906 6.85938C5.44531 6.5 5.65625 6.1875 5.92188 5.92188C6.1875 5.65625 6.4974 5.44792 6.85156 5.29688C7.21094 5.14062 7.59375 5.0625 8 5.0625ZM8 10.0625C8.28646 10.0625 8.55469 10.0104 8.80469 9.90625C9.05469 9.79688 9.27344 9.64844 9.46094 9.46094C9.64844 9.27344 9.79427 9.05469 9.89844 8.80469C10.0078 8.55469 10.0625 8.28646 10.0625 8C10.0625 7.71354 10.0078 7.44531 9.89844 7.19531C9.79427 6.94531 9.64844 6.72656 9.46094 6.53906C9.27344 6.35156 9.05469 6.20573 8.80469 6.10156C8.55469 5.99219 8.28646 5.9375 8 5.9375C7.71354 5.9375 7.44531 5.99219 7.19531 6.10156C6.94531 6.20573 6.72656 6.35156 6.53906 6.53906C6.35156 6.72656 6.20312 6.94531 6.09375 7.19531C5.98958 7.44531 5.9375 7.71354 5.9375 8C5.9375 8.28646 5.98958 8.55469 6.09375 8.80469C6.20312 9.05469 6.35156 9.27344 6.53906 9.46094C6.72656 9.64844 6.94531 9.79688 7.19531 9.90625C7.44531 10.0104 7.71354 10.0625 8 10.0625Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
@@ -1,10 +0,0 @@
|
||||
<svg id="a2f0dd32-c564-48d6-97d7-86323bfba35b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18">
|
||||
<defs>
|
||||
<linearGradient id="b863127b-2eb8-42a1-a46b-989a6a8d258c" x1="9" y1="18" x2="9" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#32bedd" />
|
||||
<stop offset="0.576" stop-color="#32ceef" />
|
||||
<stop offset="1" stop-color="#32d4f5" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M18,9.972V7.92l-.288-.108-2.2-.72-.576-1.4,1.116-2.376-1.44-1.44-.288.144L12.276,3.06l-1.4-.576L9.972,0H7.92L7.812.288l-.72,2.2-1.4.576L3.348,1.944l-1.44,1.44.144.288L3.1,5.724l-.576,1.4L0,8.028V10.08l.288.108,2.2.72.576,1.4L1.944,14.688l1.44,1.44.288-.144L5.724,14.94l1.4.576L8.028,18H10.08l.108-.288.72-2.2,1.4-.576,2.376,1.116,1.44-1.44-.144-.288L14.94,12.276l.576-1.4ZM9,12.95A3.95,3.95,0,1,1,12.95,9,3.947,3.947,0,0,1,9,12.95Z" fill="url(#b863127b-2eb8-42a1-a46b-989a6a8d258c)" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 909 B |
@@ -1,2 +1 @@
|
||||
title: Azure Arc Data Services
|
||||
description: A collection of notebooks to support Azure Arc Data Services.
|
||||
title: Azure Arc Data Services
|
||||
@@ -1,12 +1,10 @@
|
||||
- title: Welcome
|
||||
url: /readme
|
||||
not_numbered: true
|
||||
- title: Search
|
||||
search: true
|
||||
- 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
|
||||
- title: Postgres
|
||||
url: /postgres/readme
|
||||
not_numbered: true
|
||||
sections:
|
||||
- title: TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter
|
||||
url: postgres/tsg100-troubleshoot-postgres
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
- This chapter contains notebooks for troubleshooting Postgres on Azure Arc
|
||||
|
||||
## Notebooks in this Chapter
|
||||
- [TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter](tsg100-troubleshoot-postgres.ipynb)
|
||||
|
||||
|
||||
[Home](../readme.md)
|
||||
|
||||
## Notebooks in this Chapter
|
||||
|
||||
- [TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter](../postgres/tsg100-troubleshoot-postgres.ipynb)
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
- 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
|
||||
@@ -2,7 +2,11 @@
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter\n",
|
||||
"===================================================================\n",
|
||||
@@ -35,14 +39,17 @@
|
||||
"# 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",
|
||||
"metadata": {},
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"### Common functions\n",
|
||||
"\n",
|
||||
@@ -63,7 +70,6 @@
|
||||
"import sys\n",
|
||||
"import os\n",
|
||||
"import re\n",
|
||||
"import json\n",
|
||||
"import platform\n",
|
||||
"import shlex\n",
|
||||
"import shutil\n",
|
||||
@@ -76,11 +82,7 @@
|
||||
"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",
|
||||
"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",
|
||||
"def run(cmd, return_output=False, no_output=False, retry_count=0, base64_decode=False, return_as_json=False):\n",
|
||||
" \"\"\"Run shell command, stream stdout, print stderr and optionally return output\n",
|
||||
"\n",
|
||||
" NOTES:\n",
|
||||
@@ -103,13 +105,6 @@
|
||||
" 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",
|
||||
@@ -172,7 +167,12 @@
|
||||
" 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,8 +219,6 @@
|
||||
" 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",
|
||||
@@ -276,25 +274,22 @@
|
||||
" 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 < MAX_RETRIES:\n",
|
||||
" if retry_count \u003c 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",
|
||||
" return output\n",
|
||||
" else:\n",
|
||||
" return\n",
|
||||
" if base64_decode:\n",
|
||||
" import base64\n",
|
||||
" return base64.b64decode(output).decode('utf-8')\n",
|
||||
" else:\n",
|
||||
" return output\n",
|
||||
"\n",
|
||||
" elapsed = datetime.datetime.now().replace(microsecond=0) - start_time\n",
|
||||
"\n",
|
||||
@@ -311,78 +306,31 @@
|
||||
" print(f'\\nSUCCESS: {elapsed}s elapsed.\\n')\n",
|
||||
"\n",
|
||||
" if 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",
|
||||
" if base64_decode:\n",
|
||||
" import base64\n",
|
||||
" return base64.b64decode(output).decode('utf-8')\n",
|
||||
" else:\n",
|
||||
" return output\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"print('Common functions defined successfully.')\n",
|
||||
"\n",
|
||||
"# Hints for binary (transient fault) retry, (known) error and install guide\n",
|
||||
"# Hints for tool retry (on transient fault), known errors and install guide\n",
|
||||
"#\n",
|
||||
"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']}"
|
||||
"retry_hints = {}\n",
|
||||
"error_hints = {}\n",
|
||||
"install_hint = {}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"print('Common functions defined successfully.')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"### Get Postgres server"
|
||||
]
|
||||
@@ -400,10 +348,11 @@
|
||||
"# 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 and version:\n",
|
||||
" server = json.loads(run(f'kubectl get postgresql-{version} -n {namespace} {name} -o json', return_output=True))\n",
|
||||
"if namespace and name:\n",
|
||||
" server = json.loads(run(f'kubectl get postgresqls -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",
|
||||
@@ -415,19 +364,18 @@
|
||||
"\n",
|
||||
" pad = math.floor(math.log10(len(servers)) + 1) + 3\n",
|
||||
" for i, s in enumerate(servers):\n",
|
||||
" print(f'{f\"[{i+1}]\":<{pad}}{full_name(s)}')\n",
|
||||
" print(f'{f\"[{i+1}]\":\u003c{pad}}{full_name(s)}')\n",
|
||||
"\n",
|
||||
" while True:\n",
|
||||
" try:\n",
|
||||
" i = int(input('Enter the index of a server to troubleshoot: '))\n",
|
||||
" i = int(input('Enter the index of a server'))\n",
|
||||
" except ValueError:\n",
|
||||
" continue\n",
|
||||
"\n",
|
||||
" if i >= 1 and i <= len(servers):\n",
|
||||
" if i \u003e= 1 and i \u003c= 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}'))"
|
||||
@@ -435,7 +383,11 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"### Summarize all resources"
|
||||
]
|
||||
@@ -443,13 +395,15 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"uid = server['metadata']['uid']\n",
|
||||
"\n",
|
||||
"display(Markdown(f'#### Server summary'))\n",
|
||||
"run(f'kubectl get postgresql-{version} -n {namespace} {name}')\n",
|
||||
"run(f'kubectl get postgresqls -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}')"
|
||||
@@ -457,7 +411,11 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"### Troubleshoot the server"
|
||||
]
|
||||
@@ -465,16 +423,22 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"display(Markdown(f'#### Troubleshooting server {namespace}.{name}'))\n",
|
||||
"run(f'kubectl describe postgresql-{version} -n {namespace} {name}')"
|
||||
"run(f'kubectl describe postgresqls -n {namespace} {name}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"### Troubleshoot the pods"
|
||||
]
|
||||
@@ -482,7 +446,9 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"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",
|
||||
@@ -505,7 +471,11 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"### Troubleshoot the containers"
|
||||
]
|
||||
@@ -513,7 +483,9 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Summarize and get logs from each container\n",
|
||||
@@ -521,7 +493,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) < 2 else \"s\"} '\n",
|
||||
" display(Markdown(f'#### Troubleshooting {len(cons)} container{\"\" if len(cons) \u003c 2 else \"s\"} '\n",
|
||||
" f'containers for pod {namespace}.{pod_name}'))\n",
|
||||
"\n",
|
||||
" for i, con in enumerate(cons):\n",
|
||||
@@ -537,14 +509,18 @@
|
||||
" 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 > 0:\n",
|
||||
" if con_restarts \u003e 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",
|
||||
"metadata": {},
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"### Troubleshoot the PersistentVolumeClaims"
|
||||
]
|
||||
@@ -552,7 +528,9 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"display(Markdown(f'#### Troubleshooting PersistentVolumeClaims'))\n",
|
||||
@@ -562,10 +540,12 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"print('Notebook execution complete.')"
|
||||
"print(\"Notebook execution is complete.\")"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -576,20 +556,36 @@
|
||||
"name": "python3",
|
||||
"display_name": "Python 3"
|
||||
},
|
||||
"azdata": {
|
||||
"pansop": {
|
||||
"related": "",
|
||||
"test": {
|
||||
"ci": false,
|
||||
"gci": false
|
||||
},
|
||||
"contract": {
|
||||
"requires": {
|
||||
"kubectl": {
|
||||
"installed": true
|
||||
}
|
||||
"strategy": "",
|
||||
"types": null,
|
||||
"disable": {
|
||||
"reason": "",
|
||||
"workitems": null,
|
||||
"types": null
|
||||
}
|
||||
},
|
||||
"side_effects": false
|
||||
}
|
||||
"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": []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# Azure Arc Data Services Jupyter Book
|
||||
|
||||
## Chapters
|
||||
|
||||
1. [Postgres](postgres/readme.md) - notebooks for troubleshooting Postgres on Azure Arc.
|
||||
@@ -132,13 +132,7 @@
|
||||
" 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.')\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\""
|
||||
" sys.exit(f'Passwords do not match.')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "e7e10828-6cae-45af-8c2f-1484b6d4f9ac",
|
||||
@@ -188,7 +182,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 {arc_data_controller_connectivity_mode} -n {arc_data_controller_name} -ns {arc_data_controller_namespace} -s {arc_subscription} -g {arc_resource_group} -l {arc_data_controller_location} -sc {arc_data_controller_storage_class} --profile-name {arc_profile}')\n",
|
||||
"run_command(f'azdata arc dc create --connectivity-mode Indirect -n {arc_data_controller_name} -ns {arc_data_controller_namespace} -s {arc_subscription} -g {arc_resource_group} -l {arc_data_controller_location} -sc {arc_data_controller_storage_class} --profile-name {arc_profile}')\n",
|
||||
"print(f'Azure Arc Data Controller: {arc_data_controller_name} created.') "
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
@@ -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}{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} --replicas {sql_replicas}{cores_request_option}{cores_limit_option}{memory_request_option}{memory_limit_option}'\n",
|
||||
"out=run_command()"
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
"name": "arc",
|
||||
"displayName": "%arc.displayName%",
|
||||
"description": "%arc.description%",
|
||||
"version": "0.7.2",
|
||||
"version": "0.9.3",
|
||||
"publisher": "Microsoft",
|
||||
"preview": true,
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
|
||||
"icon": "images/extension.png",
|
||||
"engines": {
|
||||
"vscode": "*",
|
||||
"azdata": ">=1.26.0"
|
||||
"azdata": ">=1.28.0"
|
||||
},
|
||||
"activationEvents": [
|
||||
"onCommand:arc.connectToController",
|
||||
@@ -132,7 +132,7 @@
|
||||
],
|
||||
"resourceDeploymentTypes": [
|
||||
{
|
||||
"name": "arc.control.create",
|
||||
"name": "arc-controller",
|
||||
"displayName": "%resource.type.azure.arc.display.name%",
|
||||
"description": "%resource.type.azure.arc.description%",
|
||||
"platforms": "*",
|
||||
@@ -144,6 +144,7 @@
|
||||
],
|
||||
"providers": [
|
||||
{
|
||||
"name": "arc-controller",
|
||||
"notebookWizard": {
|
||||
"notebook": "./notebooks/arcDeployment/deploy.arc.data.controller.ipynb",
|
||||
"type": "new-arc-control-plane",
|
||||
@@ -240,99 +241,6 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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%"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -599,12 +507,6 @@
|
||||
"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)"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -618,7 +520,7 @@
|
||||
},
|
||||
{
|
||||
"name": "azdata",
|
||||
"version": "20.2.6"
|
||||
"version": "20.3.2"
|
||||
}
|
||||
],
|
||||
"when": true
|
||||
@@ -626,7 +528,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "arc.postgres",
|
||||
"name": "arc-postgres",
|
||||
"displayName": "%resource.type.arc.postgres.display.name%",
|
||||
"description": "%resource.type.arc.postgres.description%",
|
||||
"platforms": "*",
|
||||
@@ -637,6 +539,7 @@
|
||||
],
|
||||
"providers": [
|
||||
{
|
||||
"name": "arc-postgres",
|
||||
"notebookWizard": {
|
||||
"notebook": "./notebooks/arcDeployment/deploy.postgres.existing.arc.ipynb",
|
||||
"doneAction": {
|
||||
@@ -661,7 +564,7 @@
|
||||
"fields": [
|
||||
{
|
||||
"label": "%arc.controller%",
|
||||
"variableName": "",
|
||||
"variableName": "CONTROLLER_NAME",
|
||||
"type": "options",
|
||||
"editable": false,
|
||||
"required": true,
|
||||
@@ -869,7 +772,7 @@
|
||||
},
|
||||
{
|
||||
"name": "azdata",
|
||||
"version": "20.2.6"
|
||||
"version": "20.3.2"
|
||||
}
|
||||
],
|
||||
"when": "true"
|
||||
@@ -912,6 +815,7 @@
|
||||
"SQL Server"
|
||||
],
|
||||
"provider": {
|
||||
"name": "azure-sql-mi_arc-mi",
|
||||
"notebookWizard": {
|
||||
"notebook": "./notebooks/arcDeployment/deploy.sql.existing.arc.ipynb",
|
||||
"doneAction": {
|
||||
@@ -936,7 +840,7 @@
|
||||
"fields": [
|
||||
{
|
||||
"label": "%arc.controller%",
|
||||
"variableName": "",
|
||||
"variableName": "CONTROLLER_NAME",
|
||||
"type": "options",
|
||||
"editable": false,
|
||||
"required": true,
|
||||
@@ -996,6 +900,21 @@
|
||||
{
|
||||
"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%",
|
||||
@@ -1082,7 +1001,7 @@
|
||||
},
|
||||
{
|
||||
"name": "azdata",
|
||||
"version": "20.2.6"
|
||||
"version": "20.3.2"
|
||||
}
|
||||
],
|
||||
"when": "mi-type=arc-mi"
|
||||
@@ -1112,6 +1031,11 @@
|
||||
"when": "mi-type=arc-mi"
|
||||
}
|
||||
}
|
||||
],
|
||||
"resourceDeploymentOptionsSources": [
|
||||
{
|
||||
"id": "arc.controllers"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -1127,7 +1051,7 @@
|
||||
"@types/sinon": "^9.0.4",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@types/yamljs": "^0.2.31",
|
||||
"@microsoft/azdata-test": "^1.4.0",
|
||||
"@microsoft/azdata-test": "^1.5.0",
|
||||
"mocha": "^5.2.0",
|
||||
"mocha-junit-reporter": "^1.17.0",
|
||||
"mocha-multi-reporters": "^1.1.7",
|
||||
|
||||
@@ -22,8 +22,7 @@
|
||||
"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 and Connectivity Configuration",
|
||||
"arc.data.controller.connectivitymode.description": "Select the connectivity mode for the controller.",
|
||||
"arc.data.controller.create.azureconfig.title": "Azure Configuration",
|
||||
"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.",
|
||||
@@ -38,18 +37,6 @@
|
||||
"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",
|
||||
@@ -97,6 +84,8 @@
|
||||
"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",
|
||||
|
||||
@@ -10,6 +10,7 @@ import * as loc from '../localizedConstants';
|
||||
import { throwUnless } from './utils';
|
||||
export interface KubeClusterContext {
|
||||
name: string;
|
||||
namespace?: string;
|
||||
isCurrentContext: boolean;
|
||||
}
|
||||
|
||||
@@ -18,7 +19,7 @@ export interface KubeClusterContext {
|
||||
*
|
||||
* @param configFile
|
||||
*/
|
||||
export function getKubeConfigClusterContexts(configFile: string): Promise<KubeClusterContext[]> {
|
||||
export function getKubeConfigClusterContexts(configFile: string): KubeClusterContext[] {
|
||||
const config: any = yamljs.load(configFile);
|
||||
const rawContexts = <any[]>config['contexts'];
|
||||
throwUnless(rawContexts && rawContexts.length, loc.noContextFound(configFile));
|
||||
@@ -26,16 +27,16 @@ export function getKubeConfigClusterContexts(configFile: string): Promise<KubeCl
|
||||
throwUnless(currentContext, loc.noCurrentContextFound(configFile));
|
||||
const contexts: KubeClusterContext[] = [];
|
||||
rawContexts.forEach(rawContext => {
|
||||
const name = <string>rawContext['name'];
|
||||
const name = rawContext.name as string;
|
||||
const namespace = rawContext.context.namespace as string;
|
||||
throwUnless(name, loc.noNameInContext(configFile));
|
||||
if (name) {
|
||||
contexts.push({
|
||||
name: name,
|
||||
isCurrentContext: name === currentContext
|
||||
});
|
||||
}
|
||||
contexts.push({
|
||||
name: name,
|
||||
namespace: namespace,
|
||||
isCurrentContext: name === currentContext
|
||||
});
|
||||
});
|
||||
return Promise.resolve(contexts);
|
||||
return contexts;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,22 +48,23 @@ export function getKubeConfigClusterContexts(configFile: string): Promise<KubeCl
|
||||
*
|
||||
*
|
||||
* @param clusterContexts
|
||||
* @param previousClusterContext
|
||||
* @param previousClusterContextName
|
||||
* @param throwIfNotFound
|
||||
*/
|
||||
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
|
||||
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
|
||||
return previousClusterContext;
|
||||
} else {
|
||||
if (throwIfNotFound) {
|
||||
throw new Error(loc.clusterContextNotFound(previousClusterContext));
|
||||
throw new Error(loc.clusterContextNotFound(previousClusterContextName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if not previousClusterContext or throwIfNotFound was false when previousCLusterContext was not found in the clusterContexts
|
||||
const currentClusterContext = clusterContexts.find(c => c.isCurrentContext)?.name;
|
||||
const currentClusterContext = clusterContexts.find(c => c.isCurrentContext);
|
||||
throwUnless(currentClusterContext !== undefined, loc.noCurrentClusterContext);
|
||||
return currentClusterContext;
|
||||
}
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
/**
|
||||
* Deferred promise
|
||||
*/
|
||||
export class Deferred<T> {
|
||||
export class Deferred<T = void> {
|
||||
promise: Promise<T>;
|
||||
resolve!: (value?: T | PromiseLike<T>) => void;
|
||||
resolve!: (value: T | PromiseLike<T>) => void;
|
||||
reject!: (reason?: any) => void;
|
||||
constructor() {
|
||||
this.promise = new Promise<T>((resolve, reject) => {
|
||||
this.resolve = <any>resolve;
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ export function getDatabaseStateDisplayText(state: string): string {
|
||||
* @returns Promise resolving to the user's input if it passed validation,
|
||||
* or undefined if the input box was closed for any other reason
|
||||
*/
|
||||
async function promptInputBox(title: string, options: vscode.InputBoxOptions): Promise<string> {
|
||||
async function promptInputBox(title: string, options: vscode.InputBoxOptions): Promise<string | undefined> {
|
||||
const inputBox = vscode.window.createInputBox();
|
||||
inputBox.title = title;
|
||||
inputBox.prompt = options.prompt;
|
||||
@@ -118,7 +118,7 @@ async function promptInputBox(title: string, options: vscode.InputBoxOptions): P
|
||||
inputBox.value = options.value ?? '';
|
||||
inputBox.ignoreFocusOut = options.ignoreFocusOut ?? false;
|
||||
|
||||
return new Promise<any>(resolve => {
|
||||
return new Promise(resolve => {
|
||||
let valueAccepted = false;
|
||||
inputBox.onDidAccept(async () => {
|
||||
if (options.validateInput) {
|
||||
@@ -184,6 +184,30 @@ 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
|
||||
@@ -198,12 +222,16 @@ export function getErrorMessage(error: any, useMessageWithLink: boolean = false)
|
||||
|
||||
/**
|
||||
* Parses an address into its separate ip and port values. Address must be in the form <ip>:<port>
|
||||
* or <ip>,<port>
|
||||
* @param address The address to parse
|
||||
*/
|
||||
export function parseIpAndPort(address: string): { ip: string, port: string } {
|
||||
const sections = address.split(':');
|
||||
let sections = address.split(':');
|
||||
if (sections.length !== 2) {
|
||||
throw new Error(`Invalid address format for ${address}. Address must be in the form <ip>:<port>`);
|
||||
sections = address.split(',');
|
||||
if (sections.length !== 2) {
|
||||
throw new Error(`Invalid address format for ${address}. Address must be in the form <ip>:<port> or <ip>,<port>`);
|
||||
}
|
||||
}
|
||||
return {
|
||||
ip: sections[0],
|
||||
|
||||
@@ -45,7 +45,8 @@ export class IconPathHelper {
|
||||
public static discard: IconPath;
|
||||
public static fail: IconPath;
|
||||
public static information: IconPath;
|
||||
public static gear: IconPath;
|
||||
public static gearBlue: IconPath;
|
||||
public static gearGray: IconPath;
|
||||
|
||||
public static setExtensionContext(context: vscode.ExtensionContext) {
|
||||
IconPathHelper.context = context;
|
||||
@@ -141,9 +142,13 @@ export class IconPathHelper {
|
||||
light: context.asAbsolutePath('images/information.svg'),
|
||||
dark: context.asAbsolutePath('images/information.svg'),
|
||||
};
|
||||
IconPathHelper.gear = {
|
||||
light: context.asAbsolutePath('images/gear.svg'),
|
||||
dark: context.asAbsolutePath('images/gear.svg'),
|
||||
IconPathHelper.gearBlue = {
|
||||
light: context.asAbsolutePath('images/gear-colored-blue.svg'),
|
||||
dark: context.asAbsolutePath('images/gear-colored-blue.svg'),
|
||||
};
|
||||
IconPathHelper.gearGray = {
|
||||
light: context.asAbsolutePath('images/gear-colored-gray.svg'),
|
||||
dark: context.asAbsolutePath('images/gear-colored-gray.svg'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<arc.IE
|
||||
vscode.window.registerTreeDataProvider('azureArc', treeDataProvider);
|
||||
|
||||
vscode.commands.registerCommand('arc.createController', async () => {
|
||||
await vscode.commands.executeCommand('azdata.resource.deploy', 'arc.control.create', ['arc.control.create']);
|
||||
await vscode.commands.executeCommand('azdata.resource.deploy', 'arc-controller', ['arc-controller']);
|
||||
});
|
||||
|
||||
vscode.commands.registerCommand('arc.connectToController', async () => {
|
||||
|
||||
@@ -24,6 +24,8 @@ export const settings = localize('arc.settings', "Settings");
|
||||
export const security = localize('arc.security', "Security");
|
||||
export const computeAndStorage = localize('arc.computeAndStorage', "Compute + Storage");
|
||||
export const nodeParameters = localize('arc.nodeParameters', "Node Parameters");
|
||||
export const coordinatorNodeParameters = localize('arc.coordinatorNodeParameters', "Coordinator Node Parameters");
|
||||
export const workerNodeParameters = localize('arc.workerNodeParameters', "Worker Node Parameters");
|
||||
export const compute = localize('arc.compute', "Compute");
|
||||
export const backup = localize('arc.backup', "Backup");
|
||||
export const newSupportRequest = localize('arc.newSupportRequest', "New support request");
|
||||
@@ -66,7 +68,9 @@ export const feedback = localize('arc.feedback', "Feedback");
|
||||
export const selectConnectionString = localize('arc.selectConnectionString', "Select from available client connection strings below.");
|
||||
export const addingWorkerNodes = localize('arc.addingWorkerNodes', "adding worker nodes");
|
||||
export const workerNodesDescription = localize('arc.workerNodesDescription', "Expand your server group and scale your database by adding worker nodes.");
|
||||
export const postgresConfigurationInformation = localize('arc.postgres.configurationInformation', "You can configure the number of CPU cores and storage size that will apply to both worker nodes and coordinator node. Each worker node will have the same configuration. Adjust the number of CPU cores and memory settings for your server group.");
|
||||
export const postgresConfigurationInformation = localize('arc.postgres.configurationInformation', "You can configure the number of CPU cores and storage size that will apply to both worker nodes and coordinator node. Each worker node will have the same configuration. Adjust the number of CPU cores and memory settings for your server group. To reset the requests and/or limits, pass in empty value.");
|
||||
export const workerNodesConfigurationInformation = localize('arc.workerNodesConfigurationInformation', "You can configure the number of CPU cores and storage size that will apply to all worker nodes. Adjust the number of CPU cores and memory settings for your server group. To reset the requests and/or limits, pass in empty value.");
|
||||
export const coordinatorNodeConfigurationInformation = localize('arc.coordinatorNodeConfigurationInformation', "You can configure the number of CPU cores and storage size that will apply to the coordinator node. Adjust the number of CPU cores and memory settings for your server group. To reset the requests and/or limits, pass in empty value.");
|
||||
export const workerNodesInformation = localize('arc.workerNodeInformation', "In preview it is not possible to reduce the number of worker nodes. Please refer to documentation linked above for more information.");
|
||||
export const vCores = localize('arc.vCores', "vCores");
|
||||
export const ram = localize('arc.ram', "RAM");
|
||||
@@ -75,7 +79,10 @@ 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");
|
||||
export const pending = localize('arc.pending', "Pending");
|
||||
export const failed = localize('arc.failed', "Failed");
|
||||
export const unknown = localize('arc.unknown', "Unknown");
|
||||
@@ -90,13 +97,18 @@ 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");
|
||||
@@ -128,12 +140,16 @@ 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");
|
||||
export const confirmNewPassword = localize('arc.confirmNewPassword', "Confirm the new password");
|
||||
export const learnAboutPostgresClients = localize('arc.learnAboutPostgresClients', "Learn more about Azure PostgreSQL Hyperscale client interfaces");
|
||||
export const nodeParametersDescription = localize('arc.nodeParametersDescription', " These server parameters of the Coordinator node and the Worker nodes can be set to custom (non-default) values. Search to find parameters.");
|
||||
export const coordinatorNodeParametersDescription = localize('arc.coordinatorNodeParametersDescription', " These server parameters of the Coordinator node can be set to custom (non-default) values. Search to find parameters.");
|
||||
export const workerNodesParametersDescription = localize('arc.workerNodesParametersDescription', " These server parameters of the Worker nodes can be set to custom (non-default) values. Search to find parameters.");
|
||||
export const learnAboutNodeParameters = localize('arc.learnAboutNodeParameters', "Learn more about database engine settings for Azure Arc enabled PostgreSQL Hyperscale");
|
||||
export const noNodeParametersFound = localize('arc.noNodeParametersFound', "No worker server parameters found...");
|
||||
export const searchToFilter = localize('arc.searchToFilter', "Search to filter items...");
|
||||
@@ -144,35 +160,41 @@ 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");
|
||||
export const workerNodes = localize('arc.workerNodes', "Worker Nodes");
|
||||
export const coordinatorNode = localize('arc.coordinatorNode', "Coordinator Node");
|
||||
export const storagePerNode = localize('arc.storagePerNode', "storage per node");
|
||||
export const workerNodeCount = localize('arc.workerNodeCount', "Worker node count:");
|
||||
export const configurationPerNode = localize('arc.configurationPerNode', "Configuration (per node)");
|
||||
export const configuration = localize('arc.configurationCoordinatorNode', "Configuration");
|
||||
export const coresLimit = localize('arc.coresLimit', "CPU limit:");
|
||||
export const coresRequest = localize('arc.coresRequest', "CPU request:");
|
||||
export const memoryLimit = localize('arc.memoryLimit', "Memory limit (in GB):");
|
||||
export const memoryRequest = localize('arc.memoryRequest', "Memory request (in GB):");
|
||||
export const workerValidationErrorMessage = localize('arc.workerValidationErrorMessage', "The number of workers cannot be decreased.");
|
||||
export const memoryRequestValidationErrorMessage = localize('arc.memoryRequestValidationErrorMessage', "Memory request must be at least 0.25Gib");
|
||||
export const memoryLimitValidationErrorMessage = localize('arc.memoryLimitValidationErrorMessage', "Memory limit must be at least 0.25Gib");
|
||||
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 lastUpdated = localize('arc.lastUpdated', "Last updated");
|
||||
export const lastTransition = localize('arc.lastTransition', "Last transition");
|
||||
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 aren’t 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 allowedValue(value: string): string { return localize('arc.allowedValue', "Value is expected to be {0}", value); }
|
||||
export function databaseCreated(name: string): string { return localize('arc.databaseCreated', "Database {0} created", name); }
|
||||
export function deletingInstance(name: string): string { return localize('arc.deletingInstance', "Deleting instance '{0}'...", name); }
|
||||
export function installingExtension(name: string): string { return localize('arc.installingExtension', "Installing extension '{0}'...", name); }
|
||||
@@ -182,6 +204,9 @@ export function instanceDeleted(name: string): string { return localize('arc.ins
|
||||
export function instanceUpdated(name: string): string { return localize('arc.instanceUpdated', "Instance '{0}' updated", name); }
|
||||
export function copiedToClipboard(name: string): string { return localize('arc.copiedToClipboard', "{0} copied to clipboard", name); }
|
||||
export function clickTheTroubleshootButton(resourceType: string): string { return localize('arc.clickTheTroubleshootButton', "Click the troubleshoot button to open the Azure Arc {0} troubleshooting notebook.", resourceType); }
|
||||
export function dataStorage(value: string): string { return localize('arc.dataStorage', "{0} data", value); }
|
||||
export function logStorage(value: string): string { return localize('arc.logStorage', "{0} log", value); }
|
||||
export function backupsStorage(value: string): string { return localize('arc.backupsStorage', "{0} backups", value); }
|
||||
export function numVCores(vCores: string | undefined): string {
|
||||
if (vCores && +vCores > 0) {
|
||||
if (+vCores === 1) {
|
||||
@@ -194,13 +219,11 @@ export function numVCores(vCores: string | undefined): string {
|
||||
}
|
||||
}
|
||||
export function updated(when: string): string { return localize('arc.updated', "Updated {0}", when); }
|
||||
export function validationMin(min: number): string { return localize('arc.validationMin', "Value must be greater than or equal to {0}.", min); }
|
||||
|
||||
// Errors
|
||||
export const pgConnectionRequired = localize('arc.pgConnectionRequired', "A connection is required to show and set database engine settings.");
|
||||
export const miaaConnectionRequired = localize('arc.miaaConnectionRequired', "A connection is required to list the databases on this instance.");
|
||||
export const couldNotFindControllerRegistration = localize('arc.couldNotFindControllerRegistration', "Could not find controller registration.");
|
||||
export function outOfRange(min: string, max: string): string { return localize('arc.outOfRange', "The number must be in range {0} - {1}", min, max); }
|
||||
export function refreshFailed(error: any): string { return localize('arc.refreshFailed', "Refresh failed. {0}", getErrorMessage(error)); }
|
||||
export function resetFailed(error: any): string { return localize('arc.resetFailed', "Reset failed. {0}", getErrorMessage(error)); }
|
||||
export function openDashboardFailed(error: any): string { return localize('arc.openDashboardFailed', "Error opening dashboard. {0}", getErrorMessage(error)); }
|
||||
@@ -218,6 +241,7 @@ 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); }
|
||||
|
||||
@@ -46,6 +46,20 @@ 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);
|
||||
@@ -63,10 +77,10 @@ export class ControllerModel {
|
||||
* calls from changing the context while commands for this session are being executed.
|
||||
* @param promptReconnect
|
||||
*/
|
||||
public async acquireAzdataSession(promptReconnect: boolean = false): Promise<azdataExt.AzdataSession> {
|
||||
public async login(promptReconnect: boolean = false): Promise<void> {
|
||||
let promptForValidClusterContext: boolean = false;
|
||||
try {
|
||||
const contexts = await getKubeConfigClusterContexts(this.info.kubeConfigFilePath);
|
||||
const contexts = 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);
|
||||
@@ -100,8 +114,7 @@ export class ControllerModel {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this._azdataApi.azdata.acquireSession(this.info.url, this.info.username, this._password, this.azdataAdditionalEnvVars);
|
||||
await this._azdataApi.azdata.login({ endpoint: this.info.endpoint, namespace: this.info.namespace }, this.info.username, this._password, this.azdataAdditionalEnvVars);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,67 +128,64 @@ export class ControllerModel {
|
||||
await this.refresh(false);
|
||||
}
|
||||
}
|
||||
public async refresh(showErrors: boolean = true, promptReconnect: boolean = false): Promise<void> {
|
||||
const session = await this.acquireAzdataSession(promptReconnect);
|
||||
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);
|
||||
const newRegistrations: Registration[] = [];
|
||||
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;
|
||||
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
|
||||
};
|
||||
}));
|
||||
}),
|
||||
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);
|
||||
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
|
||||
};
|
||||
}));
|
||||
})
|
||||
]);
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
]).then(() => {
|
||||
this._registrations = newRegistrations;
|
||||
this.registrationsLastUpdated = new Date();
|
||||
this._onRegistrationsUpdated.fire(this._registrations);
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
public get endpoints(): azdataExt.DcEndpointListResult[] {
|
||||
@@ -204,6 +214,6 @@ export class ControllerModel {
|
||||
* property to for use a display label for this controller
|
||||
*/
|
||||
public get label(): string {
|
||||
return `${this.info.name} (${this.info.url})`;
|
||||
return `${this.info.name} (${this.controllerContext})`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,11 +71,9 @@ 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, session);
|
||||
const result = await this._azdataApi.azdata.arc.sql.mi.show(this.info.name, this.controllerModel.azdataAdditionalEnvVars, this.controllerModel.controllerContext);
|
||||
this._config = result.result;
|
||||
this.configLastUpdated = new Date();
|
||||
this._onConfigUpdated.fire(this._config);
|
||||
@@ -109,7 +107,6 @@ export class MiaaModel extends ResourceModel {
|
||||
this._refreshPromise.reject(err);
|
||||
throw err;
|
||||
} finally {
|
||||
session?.dispose();
|
||||
this._refreshPromise = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,8 @@ export type EngineSettingsModel = {
|
||||
|
||||
export class PostgresModel extends ResourceModel {
|
||||
private _config?: azdataExt.PostgresServerShowResult;
|
||||
public _engineSettings: EngineSettingsModel[] = [];
|
||||
public workerNodesEngineSettings: EngineSettingsModel[] = [];
|
||||
public coordinatorNodeEngineSettings: EngineSettingsModel[] = [];
|
||||
private readonly _azdataApi: azdataExt.IExtension;
|
||||
|
||||
private readonly _onConfigUpdated = new vscode.EventEmitter<azdataExt.PostgresServerShowResult>();
|
||||
@@ -52,10 +53,7 @@ export class PostgresModel extends ResourceModel {
|
||||
|
||||
/** Returns the major version of Postgres */
|
||||
public get engineVersion(): string | undefined {
|
||||
const kind = this._config?.kind;
|
||||
return kind
|
||||
? kind.substring(kind.lastIndexOf('-') + 1)
|
||||
: undefined;
|
||||
return this._config?.spec.engine.version;
|
||||
}
|
||||
|
||||
/** Returns the IP address and port of Postgres */
|
||||
@@ -75,7 +73,9 @@ export class PostgresModel extends ResourceModel {
|
||||
const ramLimit = this._config.spec.scheduling?.default?.resources?.limits?.memory;
|
||||
const cpuRequest = this._config.spec.scheduling?.default?.resources?.requests?.cpu;
|
||||
const ramRequest = this._config.spec.scheduling?.default?.resources?.requests?.memory;
|
||||
const storage = this._config.spec.storage?.data?.size;
|
||||
const dataStorage = this._config.spec.storage?.data?.size;
|
||||
const logStorage = this._config.spec.storage?.logs?.size;
|
||||
const backupsStorage = this._config.spec.storage?.backups?.size;
|
||||
|
||||
// scale.shards was renamed to scale.workers. Check both for backwards compatibility.
|
||||
const scale = this._config.spec.scale;
|
||||
@@ -93,8 +93,19 @@ export class PostgresModel extends ResourceModel {
|
||||
configuration.push(`${ramLimit ?? ramRequest!} ${loc.ram}`);
|
||||
}
|
||||
|
||||
if (storage) {
|
||||
configuration.push(`${storage} ${loc.storagePerNode}`);
|
||||
let storage: string[] = [];
|
||||
if (dataStorage) {
|
||||
storage.push(loc.dataStorage(dataStorage));
|
||||
}
|
||||
if (logStorage) {
|
||||
storage.push(loc.logStorage(logStorage));
|
||||
}
|
||||
if (backupsStorage) {
|
||||
storage.push(loc.backupsStorage(backupsStorage));
|
||||
}
|
||||
if (dataStorage || logStorage || backupsStorage) {
|
||||
storage.push(`${loc.storagePerNode}`);
|
||||
configuration.push(storage.join(' '));
|
||||
}
|
||||
|
||||
return configuration.join(', ');
|
||||
@@ -107,10 +118,8 @@ export class PostgresModel extends ResourceModel {
|
||||
return this._refreshPromise.promise;
|
||||
}
|
||||
this._refreshPromise = new Deferred();
|
||||
let session: azdataExt.AzdataSession | undefined = undefined;
|
||||
try {
|
||||
session = await this.controllerModel.acquireAzdataSession();
|
||||
this._config = (await this._azdataApi.azdata.arc.postgres.server.show(this.info.name, this.controllerModel.azdataAdditionalEnvVars, session)).result;
|
||||
this._config = (await this._azdataApi.azdata.arc.postgres.server.show(this.info.name, this.controllerModel.azdataAdditionalEnvVars, this.controllerModel.controllerContext)).result;
|
||||
this.configLastUpdated = new Date();
|
||||
this._onConfigUpdated.fire(this._config);
|
||||
this._refreshPromise.resolve();
|
||||
@@ -118,7 +127,6 @@ export class PostgresModel extends ResourceModel {
|
||||
this._refreshPromise.reject(err);
|
||||
throw err;
|
||||
} finally {
|
||||
session?.dispose();
|
||||
this._refreshPromise = undefined;
|
||||
}
|
||||
}
|
||||
@@ -137,6 +145,7 @@ export class PostgresModel extends ResourceModel {
|
||||
this._activeConnectionId = result.connectionId;
|
||||
}
|
||||
|
||||
// TODO Need to make separate calls for worker nodes and coordinator node
|
||||
const provider = azdata.dataprotocol.getProvider<azdata.QueryProvider>(this._connectionProfile!.providerName, azdata.DataProviderType.QueryProvider);
|
||||
const ownerUri = await azdata.connection.getUriForConnection(this._activeConnectionId);
|
||||
|
||||
@@ -150,7 +159,7 @@ export class PostgresModel extends ResourceModel {
|
||||
'shared_preload_libraries', 'synchronous_commit', 'ssl', 'unix_socket_permissions', 'wal_level'
|
||||
];
|
||||
|
||||
this._engineSettings = [];
|
||||
this.workerNodesEngineSettings = [];
|
||||
|
||||
engineSettings.rows.forEach(row => {
|
||||
let rowValues = row.map(c => c.displayValue);
|
||||
@@ -166,12 +175,12 @@ export class PostgresModel extends ResourceModel {
|
||||
type: rowValues.shift()
|
||||
};
|
||||
|
||||
this._engineSettings.push(result);
|
||||
this.workerNodesEngineSettings.push(result);
|
||||
}
|
||||
});
|
||||
|
||||
this.engineSettingsLastUpdated = new Date();
|
||||
this._onEngineSettingsUpdated.fire(this._engineSettings);
|
||||
this._onEngineSettingsUpdated.fire(this.workerNodesEngineSettings);
|
||||
}
|
||||
|
||||
protected createConnectionProfile(): azdata.IConnectionProfile {
|
||||
|
||||
@@ -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.url;
|
||||
case 'endpoint': return controller.info.endpoint || '';
|
||||
case 'username': return controller.info.username;
|
||||
case 'kubeConfig': return controller.info.kubeConfigFilePath;
|
||||
case 'clusterContext': return controller.info.kubeClusterContext;
|
||||
|
||||
@@ -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(await getKubeConfigClusterContexts(configFile), 'getKubeConfigClusterContexts');
|
||||
verifyContexts(getKubeConfigClusterContexts(configFile), 'getKubeConfigClusterContexts');
|
||||
});
|
||||
it('throws error when unable to load config file', async () => {
|
||||
const error = new Error('unknown error accessing file');
|
||||
|
||||
@@ -10,77 +10,81 @@ import * as azdataExt from 'azdata-ext';
|
||||
*/
|
||||
export class FakeAzdataApi implements azdataExt.IAzdataApi {
|
||||
|
||||
public postgresInstances: azdataExt.PostgresServerListResult[] = [];
|
||||
public miaaInstances: azdataExt.SqlMiListResult[] = [];
|
||||
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;
|
||||
}
|
||||
|
||||
//
|
||||
// API Implementation
|
||||
//
|
||||
public get arc() {
|
||||
const self = this;
|
||||
return {
|
||||
dc: {
|
||||
create(_namespace: string, _name: string, _connectivityMode: string, _resourceGroup: string, _location: string, _subscription: string, _profileName?: string, _storageClass?: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
|
||||
endpoint: {
|
||||
async list(): Promise<azdataExt.AzdataOutput<azdataExt.DcEndpointListResult[]>> { return <any>{ result: [] }; }
|
||||
},
|
||||
config: {
|
||||
list(): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigListResult[]>> { throw new Error('Method not implemented.'); },
|
||||
async show(): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigShowResult>> { return <any>{ result: undefined! }; }
|
||||
}
|
||||
},
|
||||
postgres: {
|
||||
server: {
|
||||
delete(_name: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
|
||||
async list(): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerListResult[]>> { return <any>{ result: self.postgresInstances }; },
|
||||
show(_name: string): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerShowResult>> { throw new Error('Method not implemented.'); },
|
||||
edit(
|
||||
_name: string,
|
||||
_args: {
|
||||
adminPassword?: boolean,
|
||||
coresLimit?: string,
|
||||
coresRequest?: string,
|
||||
engineSettings?: string,
|
||||
extensions?: string,
|
||||
memoryLimit?: string,
|
||||
memoryRequest?: string,
|
||||
noWait?: boolean,
|
||||
port?: number,
|
||||
replaceEngineSettings?: boolean,
|
||||
workers?: number
|
||||
},
|
||||
_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.'); }
|
||||
}
|
||||
}
|
||||
};
|
||||
return this._arcApi;
|
||||
}
|
||||
getPath(): Promise<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
login(_endpoint: string, _username: string, _password: string): Promise<azdataExt.AzdataOutput<void>> {
|
||||
login(_endpointOrNamespace: azdataExt.EndpointOrNamespace, _username: string, _password: string, _additionalEnvVars: azdataExt.AdditionalEnvVars = {}, _azdataContext?: 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.');
|
||||
}
|
||||
|
||||
@@ -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(), url: '', kubeConfigFilePath: '', kubeClusterContext: '', name: '', username: '', rememberPassword: false, resources: [] }, info);
|
||||
const _info: ControllerInfo = Object.assign({ id: uuid(), endpoint: '', kubeConfigFilePath: '', kubeClusterContext: '', name: '', namespace: '', username: '', rememberPassword: false, resources: [] }, info);
|
||||
super(treeDataProvider!, _info, password);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,20 @@ 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();
|
||||
@@ -39,15 +53,15 @@ describe('ControllerModel', function (): void {
|
||||
|
||||
beforeEach(function (): void {
|
||||
sinon.stub(ConnectToControllerDialog.prototype, 'showDialog');
|
||||
sinon.stub(kubeUtils, 'getKubeConfigClusterContexts').resolves([{ name: 'currentCluster', isCurrentContext: true }]);
|
||||
sinon.stub(kubeUtils, 'getKubeConfigClusterContexts').returns([{ 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), { 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));
|
||||
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo());
|
||||
await should(model.login()).be.rejectedWith(new UserCancelledError(loc.userCancelledError));
|
||||
});
|
||||
|
||||
it('Reads password from cred store', async function (): Promise<void> {
|
||||
@@ -62,13 +76,13 @@ describe('ControllerModel', function (): void {
|
||||
|
||||
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
|
||||
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
|
||||
azdataMock.setup(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
|
||||
azdataMock.setup(x => x.login(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), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
|
||||
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo());
|
||||
|
||||
await model.acquireAzdataSession();
|
||||
azdataMock.verify(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
await model.login();
|
||||
azdataMock.verify(x => x.login(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> {
|
||||
@@ -83,18 +97,18 @@ describe('ControllerModel', function (): void {
|
||||
|
||||
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
|
||||
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
|
||||
azdataMock.setup(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
|
||||
azdataMock.setup(x => x.login(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), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password);
|
||||
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo(), password);
|
||||
sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password }));
|
||||
|
||||
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: [] });
|
||||
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo());
|
||||
|
||||
await model.acquireAzdataSession();
|
||||
azdataMock.verify(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
await model.login();
|
||||
azdataMock.verify(x => x.login(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> {
|
||||
@@ -108,19 +122,19 @@ describe('ControllerModel', function (): void {
|
||||
|
||||
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
|
||||
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
|
||||
azdataMock.setup(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
|
||||
azdataMock.setup(x => x.login(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), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password);
|
||||
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo(), password);
|
||||
const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password }));
|
||||
|
||||
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: [] });
|
||||
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo());
|
||||
|
||||
await model.acquireAzdataSession(true);
|
||||
await model.login(true);
|
||||
should(waitForCloseStub.called).be.true('waitForClose should have been called');
|
||||
azdataMock.verify(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
azdataMock.verify(x => x.login(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> {
|
||||
@@ -134,20 +148,20 @@ describe('ControllerModel', function (): void {
|
||||
|
||||
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
|
||||
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
|
||||
azdataMock.setup(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
|
||||
azdataMock.setup(x => x.login(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), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password);
|
||||
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo(), 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), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, 'originalPassword');
|
||||
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo(), 'originalPassword');
|
||||
|
||||
await model.acquireAzdataSession(true);
|
||||
await model.login(true);
|
||||
should(waitForCloseStub.called).be.true('waitForClose should have been called');
|
||||
azdataMock.verify(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
azdataMock.verify(x => x.login(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> {
|
||||
@@ -162,7 +176,7 @@ describe('ControllerModel', function (): void {
|
||||
|
||||
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
|
||||
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
|
||||
azdataMock.setup(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
|
||||
azdataMock.setup(x => x.login(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 });
|
||||
|
||||
@@ -170,27 +184,19 @@ describe('ControllerModel', function (): void {
|
||||
const originalPassword = 'originalPassword';
|
||||
const model = new ControllerModel(
|
||||
treeDataProvider,
|
||||
{
|
||||
id: uuid(),
|
||||
url: '127.0.0.1',
|
||||
kubeConfigFilePath: '/path/to/.kube/config',
|
||||
kubeClusterContext: 'currentCluster',
|
||||
username: 'admin',
|
||||
name: 'arc',
|
||||
rememberPassword: false,
|
||||
resources: []
|
||||
},
|
||||
getDefaultControllerInfo(),
|
||||
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
|
||||
url: 'newUrl',
|
||||
endpoint: 'newUrl',
|
||||
kubeConfigFilePath: '/path/to/.kube/config',
|
||||
kubeClusterContext: 'currentCluster',
|
||||
username: 'newUser',
|
||||
name: 'newName',
|
||||
namespace: 'newNamespace',
|
||||
rememberPassword: true,
|
||||
resources: []
|
||||
};
|
||||
@@ -203,7 +209,7 @@ describe('ControllerModel', function (): void {
|
||||
const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve(
|
||||
{ controllerModel: newModel, password: newPassword }));
|
||||
|
||||
await model.acquireAzdataSession(true);
|
||||
await model.login(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');
|
||||
|
||||
544
extensions/arc/src/test/models/postgresModel.test.ts
Normal file
544
extensions/arc/src/test/models/postgresModel.test.ts
Normal file
@@ -0,0 +1,544 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,163 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -18,8 +18,8 @@ describe('ConnectControllerDialog', function (): void {
|
||||
|
||||
(<{ info: ControllerInfo | undefined, description: string }[]>[
|
||||
{ info: undefined, description: 'all input' },
|
||||
{ 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 => {
|
||||
{ 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 => {
|
||||
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 = { id: uuid(), url: 'https://127.0.0.1:30080', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] };
|
||||
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: [] };
|
||||
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(), url: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
|
||||
{ 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: [] },
|
||||
'https://127.0.0.1:30081');
|
||||
});
|
||||
|
||||
it('validate appends https if missing', async function (): Promise<void> {
|
||||
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: [] },
|
||||
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: [] },
|
||||
'https://127.0.0.1:30080');
|
||||
});
|
||||
|
||||
it('validate appends default port if missing', async function (): Promise<void> {
|
||||
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: [] },
|
||||
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: [] },
|
||||
'https://127.0.0.1:30080');
|
||||
});
|
||||
|
||||
it('validate appends both port and https if missing', async function (): Promise<void> {
|
||||
await validateConnectControllerDialog({ id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
|
||||
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: [] },
|
||||
'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(), url: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: name!, username: 'sa', rememberPassword: true, resources: [] },
|
||||
{ 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: [] },
|
||||
'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(), url: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: '', username: 'sa', rememberPassword: true, resources: [] },
|
||||
{ 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: [] },
|
||||
'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.url).equal(expectedUrl);
|
||||
should(model?.controllerModel.info.endpoint).equal(expectedUrl);
|
||||
should(model?.controllerModel.info.name).equal(expectedControllerInfoName);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,20 @@ 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 {
|
||||
@@ -58,7 +72,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, { 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, getDefaultControllerInfo());
|
||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||
should(children.length).equal(1, 'Controller node should be added correctly');
|
||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||
@@ -69,12 +83,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 = { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] };
|
||||
const originalInfo: ControllerInfo = getDefaultControllerInfo();
|
||||
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 = { id: originalInfo.id, url: '1.1.1.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'new-name', username: 'admin', rememberPassword: false, resources: [] };
|
||||
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 controllerModel2 = new ControllerModel(treeDataProvider, newInfo);
|
||||
await treeDataProvider.addOrUpdateController(controllerModel2, '');
|
||||
should(children.length).equal(1, 'Shouldn\'t add duplicate controller node');
|
||||
@@ -102,18 +116,20 @@ describe('AzureArcTreeDataProvider tests', function (): void {
|
||||
return mockArcApi.object;
|
||||
});
|
||||
const fakeAzdataApi = new FakeAzdataApi();
|
||||
fakeAzdataApi.postgresInstances = [{ name: 'pg1', state: '', workers: 0 }];
|
||||
fakeAzdataApi.miaaInstances = [{ name: 'miaa1', state: '', replicas: '', serverEndpoint: '' }];
|
||||
const pgInstances = [{ name: 'pg1', state: '', workers: 0 }];
|
||||
const miaaInstances = [{ name: 'miaa1', state: '', replicas: '', serverEndpoint: '' }];
|
||||
fakeAzdataApi.postgresInstances = pgInstances;
|
||||
fakeAzdataApi.miaaInstances = miaaInstances;
|
||||
mockArcApi.setup(x => x.azdata).returns(() => fakeAzdataApi);
|
||||
|
||||
sinon.stub(vscode.extensions, 'getExtension').returns(mockArcExtension.object);
|
||||
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');
|
||||
sinon.stub(kubeUtils, 'getKubeConfigClusterContexts').returns([{ name: 'currentCluster', isCurrentContext: true }]);
|
||||
const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo(), 'mypassword');
|
||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||
const controllerNode = treeDataProvider.getControllerNode(controllerModel);
|
||||
const children = await treeDataProvider.getChildren(controllerNode);
|
||||
should(children.filter(c => c.label === fakeAzdataApi.postgresInstances[0].name).length).equal(1, 'Should have a Postgres child');
|
||||
should(children.filter(c => c.label === fakeAzdataApi.miaaInstances[0].name).length).equal(1, 'Should have a MIAA child');
|
||||
should(children.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.length).equal(2, 'Should have exactly 2 children');
|
||||
});
|
||||
});
|
||||
@@ -121,8 +137,10 @@ 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, { 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: [] });
|
||||
const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo());
|
||||
const info2 = getDefaultControllerInfo();
|
||||
info2.username = 'cloudsa';
|
||||
const controllerModel2 = new ControllerModel(treeDataProvider, info2);
|
||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||
await treeDataProvider.addOrUpdateController(controllerModel2, '');
|
||||
const children = <ControllerTreeNode[]>(await treeDataProvider.getChildren());
|
||||
@@ -139,20 +157,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, { 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, getDefaultControllerInfo());
|
||||
const openDashboardPromise = treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, '');
|
||||
await should(openDashboardPromise).be.rejected();
|
||||
});
|
||||
|
||||
it('Opening dashboard for nonexistent resource throws', async function (): Promise<void> {
|
||||
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
|
||||
const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo());
|
||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||
const openDashboardPromise = treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, '');
|
||||
await should(openDashboardPromise).be.rejected();
|
||||
});
|
||||
|
||||
it('Opening dashboard for existing resource node succeeds', async function (): Promise<void> {
|
||||
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
|
||||
const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo());
|
||||
const miaaModel = new MiaaModel(controllerModel, { name: 'miaa-1', resourceType: ResourceType.sqlManagedInstances }, undefined!, treeDataProvider);
|
||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||
const controllerNode = treeDataProvider.getControllerNode(controllerModel)!;
|
||||
|
||||
3
extensions/arc/src/typings/arc.d.ts
vendored
3
extensions/arc/src/typings/arc.d.ts
vendored
@@ -37,7 +37,8 @@ declare module 'arc' {
|
||||
id: string,
|
||||
kubeConfigFilePath: string,
|
||||
kubeClusterContext: string
|
||||
url: string,
|
||||
endpoint: string | undefined,
|
||||
namespace: string,
|
||||
name: string,
|
||||
username: string,
|
||||
rememberPassword: boolean,
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as azdata from 'azdata';
|
||||
|
||||
export abstract class Dashboard {
|
||||
|
||||
private dashboard!: azdata.window.ModelViewDashboard;
|
||||
protected dashboard!: azdata.window.ModelViewDashboard;
|
||||
|
||||
constructor(protected title: string, protected readonly name: string) { }
|
||||
|
||||
@@ -16,6 +16,10 @@ 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 => {
|
||||
|
||||
@@ -11,7 +11,7 @@ export abstract class DashboardPage extends InitializingComponent {
|
||||
|
||||
protected disposables: vscode.Disposable[] = [];
|
||||
|
||||
constructor(protected modelView: azdata.ModelView) {
|
||||
constructor(protected modelView: azdata.ModelView, protected dashboard: azdata.window.ModelViewDashboard) {
|
||||
super();
|
||||
this.disposables.push(modelView.onClosed(() => {
|
||||
// Clean up best we can
|
||||
|
||||
@@ -17,6 +17,9 @@ 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);
|
||||
@@ -26,7 +29,7 @@ export class RadioOptionsGroup {
|
||||
return this._loadingBuilder.component();
|
||||
}
|
||||
|
||||
async load(optionsInfoGetter: () => Promise<RadioOptionsInfo>): Promise<void> {
|
||||
async load(optionsInfoGetter: () => RadioOptionsInfo | Promise<RadioOptionsInfo>): Promise<void> {
|
||||
this.component().loading = true;
|
||||
this._divContainer.clearItems();
|
||||
try {
|
||||
@@ -51,6 +54,7 @@ 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);
|
||||
|
||||
@@ -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._controllerModel);
|
||||
const overviewPage = new ControllerDashboardOverviewPage(modelView, this.dashboard, this._controllerModel);
|
||||
return [
|
||||
overviewPage.tab
|
||||
];
|
||||
|
||||
@@ -35,8 +35,8 @@ export class ControllerDashboardOverviewPage extends DashboardPage {
|
||||
instanceNamespace: '-',
|
||||
};
|
||||
|
||||
constructor(modelView: azdata.ModelView, private _controllerModel: ControllerModel) {
|
||||
super(modelView);
|
||||
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _controllerModel: ControllerModel) {
|
||||
super(modelView, dashboard);
|
||||
|
||||
this._azurecoreApi = vscode.extensions.getExtension(azurecore.extension.name)?.exports;
|
||||
|
||||
@@ -147,7 +147,12 @@ export class ControllerDashboardOverviewPage extends DashboardPage {
|
||||
|
||||
this.disposables.push(
|
||||
newInstance.onDidClick(async () => {
|
||||
await vscode.commands.executeCommand('azdata.resource.deploy', 'azure-sql-mi', ['azure-sql-mi', 'arc.postgres'], { 'azure-sql-mi': { 'mi-type': ['arc-mi'] } });
|
||||
const node = this._controllerModel.treeDataProvider.getControllerNode(this._controllerModel);
|
||||
await vscode.commands.executeCommand('azdata.resource.deploy',
|
||||
'azure-sql-mi', // Default option
|
||||
['azure-sql-mi', 'arc-postgres'], // Type filter
|
||||
{ 'azure-sql-mi': { 'mi-type': ['arc-mi'] } }, // Options filter
|
||||
{ 'CONTROLLER_NAME': node?.label });
|
||||
}));
|
||||
|
||||
// Refresh
|
||||
|
||||
@@ -32,8 +32,8 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
|
||||
|
||||
private readonly _azdataApi: azdataExt.IExtension;
|
||||
|
||||
constructor(protected modelView: azdata.ModelView, private _miaaModel: MiaaModel) {
|
||||
super(modelView);
|
||||
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _miaaModel: MiaaModel) {
|
||||
super(modelView, dashboard);
|
||||
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
|
||||
|
||||
this.initializeConfigurationBoxes();
|
||||
@@ -129,19 +129,18 @@ 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, session);
|
||||
this._miaaModel.info.name, this.saveArgs, this._miaaModel.controllerModel.azdataAdditionalEnvVars, this._miaaModel.controllerModel.controllerContext);
|
||||
} catch (err) {
|
||||
this.saveButton!.enabled = true;
|
||||
throw err;
|
||||
} finally {
|
||||
session?.dispose();
|
||||
}
|
||||
|
||||
await this._miaaModel.refresh();
|
||||
try {
|
||||
await this._miaaModel.refresh();
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.refreshFailed(error));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -217,7 +216,6 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
|
||||
this.memoryLimitBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||
readOnly: false,
|
||||
min: 2,
|
||||
validationErrorMessage: loc.memoryLimitValidationErrorMessage,
|
||||
inputType: 'number',
|
||||
placeHolder: loc.loading
|
||||
}).component();
|
||||
@@ -235,7 +233,6 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
|
||||
this.memoryRequestBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||
readOnly: false,
|
||||
min: 2,
|
||||
validationErrorMessage: loc.memoryRequestValidationErrorMessage,
|
||||
inputType: 'number',
|
||||
placeHolder: loc.loading
|
||||
}).component();
|
||||
@@ -320,7 +317,6 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
|
||||
currentCPUSize = '';
|
||||
}
|
||||
|
||||
this.coresRequestBox!.validationErrorMessage = loc.validationMin(this.coresRequestBox!.min!);
|
||||
this.coresRequestBox!.placeHolder = currentCPUSize;
|
||||
this.coresRequestBox!.value = '';
|
||||
this.saveArgs.coresRequest = undefined;
|
||||
@@ -331,7 +327,6 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
|
||||
currentCPUSize = '';
|
||||
}
|
||||
|
||||
this.coresLimitBox!.validationErrorMessage = loc.validationMin(this.coresLimitBox!.min!);
|
||||
this.coresLimitBox!.placeHolder = currentCPUSize;
|
||||
this.coresLimitBox!.value = '';
|
||||
this.saveArgs.coresLimit = undefined;
|
||||
|
||||
@@ -8,7 +8,6 @@ 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';
|
||||
|
||||
@@ -17,9 +16,9 @@ export class MiaaConnectionStringsPage extends DashboardPage {
|
||||
private _keyValueContainer!: KeyValueContainer;
|
||||
private _connectionStringsMessage!: azdata.TextComponent;
|
||||
|
||||
constructor(modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) {
|
||||
super(modelView);
|
||||
this.disposables.push(this._controllerModel.onRegistrationsUpdated(_ =>
|
||||
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _miaaModel: MiaaModel) {
|
||||
super(modelView, dashboard);
|
||||
this.disposables.push(this._miaaModel.onConfigUpdated(_ =>
|
||||
this.eventuallyRunOnInitialized(() => this.updateConnectionStrings())));
|
||||
}
|
||||
|
||||
|
||||
@@ -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._controllerModel, this._miaaModel);
|
||||
const connectionStringsPage = new MiaaConnectionStringsPage(modelView, this._controllerModel, this._miaaModel);
|
||||
const computeAndStoragePage = new MiaaComputeAndStoragePage(modelView, this._miaaModel);
|
||||
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);
|
||||
return [
|
||||
overviewPage.tab,
|
||||
{
|
||||
|
||||
@@ -48,8 +48,8 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
|
||||
vCores: ''
|
||||
};
|
||||
|
||||
constructor(modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) {
|
||||
super(modelView);
|
||||
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) {
|
||||
super(modelView, dashboard);
|
||||
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
|
||||
this._azurecoreApi = vscode.extensions.getExtension(azurecore.extension.name)?.exports;
|
||||
|
||||
@@ -244,17 +244,18 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
|
||||
cancellable: false
|
||||
},
|
||||
async (_progress, _token) => {
|
||||
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();
|
||||
}
|
||||
|
||||
return await this._azdataApi.azdata.arc.sql.mi.delete(this._miaaModel.info.name, this._controllerModel.azdataAdditionalEnvVars, this._controllerModel.controllerContext);
|
||||
}
|
||||
);
|
||||
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));
|
||||
|
||||
@@ -12,30 +12,43 @@ import { DashboardPage } from '../../components/dashboardPage';
|
||||
import { PostgresModel } from '../../../models/postgresModel';
|
||||
import { convertToGibibyteString } from '../../../common/utils';
|
||||
|
||||
export type ConfigurationSpecModel = {
|
||||
workers?: number,
|
||||
workerCoresRequest?: string,
|
||||
workerCoresLimit?: string,
|
||||
workerMemoryRequest?: string,
|
||||
workerMemoryLimit?: string,
|
||||
coordinatorCoresRequest?: string,
|
||||
coordinatorCoresLimit?: string,
|
||||
coordinatorMemoryRequest?: string,
|
||||
coordinatorMemoryLimit?: string
|
||||
};
|
||||
|
||||
export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
private workerContainer?: azdata.DivContainer;
|
||||
private workerContainer!: azdata.DivContainer;
|
||||
private coordinatorContainer!: azdata.DivContainer;
|
||||
|
||||
private workerBox?: azdata.InputBoxComponent;
|
||||
private coresLimitBox?: azdata.InputBoxComponent;
|
||||
private coresRequestBox?: azdata.InputBoxComponent;
|
||||
private memoryLimitBox?: azdata.InputBoxComponent;
|
||||
private memoryRequestBox?: azdata.InputBoxComponent;
|
||||
private workerBox!: azdata.InputBoxComponent;
|
||||
private workerCoresLimitBox!: azdata.InputBoxComponent;
|
||||
private workerCoresRequestBox!: azdata.InputBoxComponent;
|
||||
private workerMemoryLimitBox!: azdata.InputBoxComponent;
|
||||
private workerMemoryRequestBox!: azdata.InputBoxComponent;
|
||||
|
||||
private discardButton?: azdata.ButtonComponent;
|
||||
private saveButton?: azdata.ButtonComponent;
|
||||
private coordinatorCoresLimitBox!: azdata.InputBoxComponent;
|
||||
private coordinatorCoresRequestBox!: azdata.InputBoxComponent;
|
||||
private coordinatorMemoryLimitBox!: azdata.InputBoxComponent;
|
||||
private coordinatorMemoryRequestBox!: azdata.InputBoxComponent;
|
||||
|
||||
private saveArgs: {
|
||||
workers?: number,
|
||||
coresLimit?: string,
|
||||
coresRequest?: string,
|
||||
memoryLimit?: string,
|
||||
memoryRequest?: string
|
||||
} = {};
|
||||
private currentConfiguration: ConfigurationSpecModel = {};
|
||||
private saveArgs: ConfigurationSpecModel = {};
|
||||
|
||||
private discardButton!: azdata.ButtonComponent;
|
||||
private saveButton!: azdata.ButtonComponent;
|
||||
|
||||
private readonly _azdataApi: azdataExt.IExtension;
|
||||
|
||||
constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) {
|
||||
super(modelView);
|
||||
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _postgresModel: PostgresModel) {
|
||||
super(modelView, dashboard);
|
||||
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
|
||||
|
||||
this.initializeConfigurationBoxes();
|
||||
@@ -61,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().withProperties<azdata.TextComponentProperties>({
|
||||
content.addItem(this.modelView.modelBuilder.text().withProps({
|
||||
value: loc.computeAndStorage,
|
||||
CSSStyles: { ...cssStyles.title }
|
||||
}).component());
|
||||
|
||||
const infoComputeStorage_p1 = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
const infoComputeStorage_p1 = this.modelView.modelBuilder.text().withProps({
|
||||
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().withProperties<azdata.TextComponentProperties>({
|
||||
const infoComputeStorage_p2 = this.modelView.modelBuilder.text().withProps({
|
||||
value: loc.postgresComputeAndStorageDescriptionPartTwo,
|
||||
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component();
|
||||
@@ -81,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().withProperties<azdata.TextComponentProperties>({
|
||||
const infoComputeStorage_p3 = this.modelView.modelBuilder.text().withProps({
|
||||
value: loc.computeAndStorageDescriptionPartThree,
|
||||
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component();
|
||||
@@ -92,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().withProperties<azdata.TextComponentProperties>({
|
||||
const infoComputeStorage_p4 = this.modelView.modelBuilder.text().withProps({
|
||||
value: loc.computeAndStorageDescriptionPartFour,
|
||||
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component();
|
||||
|
||||
const infoComputeStorage_p5 = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
const infoComputeStorage_p5 = this.modelView.modelBuilder.text().withProps({
|
||||
value: loc.computeAndStorageDescriptionPartFive,
|
||||
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component();
|
||||
|
||||
const infoComputeStorage_p6 = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
const infoComputeStorage_p6 = this.modelView.modelBuilder.text().withProps({
|
||||
value: loc.computeAndStorageDescriptionPartSix,
|
||||
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component();
|
||||
@@ -122,15 +135,26 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
.component();
|
||||
content.addItem(computeInfoAndLinks, { CSSStyles: { 'min-height': '30px' } });
|
||||
|
||||
content.addItem(this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
// Worker nodes section
|
||||
this.workerContainer = this.modelView.modelBuilder.divContainer().component();
|
||||
this.workerContainer.addItem(this.modelView.modelBuilder.text().withProps({
|
||||
value: loc.workerNodes,
|
||||
CSSStyles: { ...cssStyles.title, 'margin-top': '25px' }
|
||||
}).component());
|
||||
|
||||
this.workerContainer = this.modelView.modelBuilder.divContainer().component();
|
||||
this.workerContainer.addItems(this.createUserInputSection(), { CSSStyles: { 'min-height': '30px' } });
|
||||
this.workerContainer.addItems(this.createUserInputWorkerSection(), { CSSStyles: { 'min-height': '30px' } });
|
||||
content.addItem(this.workerContainer, { CSSStyles: { 'min-height': '30px' } });
|
||||
|
||||
// Coordinator node section
|
||||
this.coordinatorContainer = this.modelView.modelBuilder.divContainer().component();
|
||||
this.coordinatorContainer.addItem(this.modelView.modelBuilder.text().withProps({
|
||||
value: loc.coordinatorNode,
|
||||
CSSStyles: { ...cssStyles.title, 'margin-top': '25px' }
|
||||
}).component());
|
||||
this.coordinatorContainer.addItems(this.createUserInputCoordinatorSection(), { CSSStyles: { 'min-height': '30px' } });
|
||||
|
||||
// TODO unhide once once ready to make azdata calls
|
||||
// content.addItem(this.coordinatorContainer, { CSSStyles: { 'min-height': '30px' } });
|
||||
|
||||
this.initialized = true;
|
||||
|
||||
return root;
|
||||
@@ -138,7 +162,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
|
||||
protected get toolbarContainer(): azdata.ToolbarContainer {
|
||||
// Save Edits
|
||||
this.saveButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
|
||||
this.saveButton = this.modelView.modelBuilder.button().withProps({
|
||||
label: loc.saveText,
|
||||
iconPath: IconPathHelper.save,
|
||||
enabled: false
|
||||
@@ -146,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(
|
||||
{
|
||||
@@ -155,31 +179,47 @@ 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,
|
||||
this.saveArgs,
|
||||
this._postgresModel.engineVersion,
|
||||
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
|
||||
session
|
||||
);
|
||||
{
|
||||
workers: this.saveArgs.workers,
|
||||
coresRequest: this.saveArgs.workerCoresRequest,
|
||||
coresLimit: this.saveArgs.workerCoresLimit,
|
||||
memoryRequest: this.saveArgs.workerMemoryRequest,
|
||||
memoryLimit: this.saveArgs.workerMemoryLimit
|
||||
},
|
||||
this._postgresModel.controllerModel.azdataAdditionalEnvVars);
|
||||
/* TODO add second edit call for coordinator configuration
|
||||
await this._azdataApi.azdata.arc.postgres.server.edit(
|
||||
this._postgresModel.info.name,
|
||||
{
|
||||
coresRequest: this.saveArgs.coordinatorCoresRequest,
|
||||
coresLimit: this.saveArgs.coordinatorCoresLimit,
|
||||
memoryRequest: this.saveArgs.coordinatorMemoryRequest,
|
||||
memoryLimit: this.saveArgs.coordinatorMemoryLimit
|
||||
},
|
||||
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
|
||||
session
|
||||
);
|
||||
*/
|
||||
} 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();
|
||||
}
|
||||
await this._postgresModel.refresh();
|
||||
try {
|
||||
await this._postgresModel.refresh();
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.refreshFailed(error));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
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));
|
||||
@@ -187,7 +227,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
}));
|
||||
|
||||
// Discard
|
||||
this.discardButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
|
||||
this.discardButton = this.modelView.modelBuilder.button().withProps({
|
||||
label: loc.discardText,
|
||||
iconPath: IconPathHelper.discard,
|
||||
enabled: false
|
||||
@@ -195,15 +235,17 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
|
||||
this.disposables.push(
|
||||
this.discardButton.onDidClick(async () => {
|
||||
this.discardButton!.enabled = false;
|
||||
this.discardButton.enabled = false;
|
||||
try {
|
||||
this.editWorkerNodeCount();
|
||||
this.editCores();
|
||||
this.editMemory();
|
||||
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;
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -213,25 +255,27 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
]).component();
|
||||
}
|
||||
|
||||
private initializeConfigurationBoxes() {
|
||||
this.workerBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||
private initializeConfigurationBoxes(): void {
|
||||
// Worker node count
|
||||
this.workerBox = this.modelView.modelBuilder.inputBox().withProps({
|
||||
readOnly: false,
|
||||
validationErrorMessage: loc.workerValidationErrorMessage,
|
||||
inputType: 'number',
|
||||
placeHolder: loc.loading
|
||||
placeHolder: loc.loading,
|
||||
required: true
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.workerBox.onTextChanged(() => {
|
||||
if (!(this.handleOnTextChanged(this.workerBox!))) {
|
||||
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!);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.coresLimitBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||
// Worker nodes cores request
|
||||
this.workerCoresRequestBox = this.modelView.modelBuilder.inputBox().withProps({
|
||||
readOnly: false,
|
||||
min: 1,
|
||||
inputType: 'number',
|
||||
@@ -239,16 +283,19 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.coresLimitBox.onTextChanged(() => {
|
||||
if (!(this.handleOnTextChanged(this.coresLimitBox!))) {
|
||||
this.saveArgs.coresLimit = undefined;
|
||||
this.workerCoresRequestBox.onTextChanged(() => {
|
||||
if (!(this.saveValueToEdit(this.workerCoresRequestBox, this.currentConfiguration.workerCoresRequest!))) {
|
||||
this.saveArgs.workerCoresRequest = undefined;
|
||||
} else if (this.workerCoresRequestBox.value === '') {
|
||||
this.saveArgs.workerCoresRequest = '""';
|
||||
} else {
|
||||
this.saveArgs.coresLimit = this.coresLimitBox!.value;
|
||||
this.saveArgs.workerCoresRequest = this.workerCoresRequestBox.value;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.coresRequestBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||
// Worker nodes cores limit
|
||||
this.workerCoresLimitBox = this.modelView.modelBuilder.inputBox().withProps({
|
||||
readOnly: false,
|
||||
min: 1,
|
||||
inputType: 'number',
|
||||
@@ -256,67 +303,152 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.coresRequestBox.onTextChanged(() => {
|
||||
if (!(this.handleOnTextChanged(this.coresRequestBox!))) {
|
||||
this.saveArgs.coresRequest = undefined;
|
||||
this.workerCoresLimitBox.onTextChanged(() => {
|
||||
if (!(this.saveValueToEdit(this.workerCoresLimitBox, this.currentConfiguration.workerCoresLimit!))) {
|
||||
this.saveArgs.workerCoresLimit = undefined;
|
||||
} else if (this.workerCoresLimitBox.value === '') {
|
||||
this.saveArgs.workerCoresLimit = '""';
|
||||
} else {
|
||||
this.saveArgs.coresRequest = this.coresRequestBox!.value;
|
||||
this.saveArgs.workerCoresLimit = this.workerCoresLimitBox.value;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.memoryLimitBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||
// Worker nodes memory request
|
||||
this.workerMemoryRequestBox = this.modelView.modelBuilder.inputBox().withProps({
|
||||
readOnly: false,
|
||||
min: 0.25,
|
||||
validationErrorMessage: loc.memoryLimitValidationErrorMessage,
|
||||
inputType: 'number',
|
||||
placeHolder: loc.loading
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.memoryLimitBox.onTextChanged(() => {
|
||||
if (!(this.handleOnTextChanged(this.memoryLimitBox!))) {
|
||||
this.saveArgs.memoryLimit = undefined;
|
||||
this.workerMemoryRequestBox.onTextChanged(() => {
|
||||
if (!(this.saveValueToEdit(this.workerMemoryRequestBox, this.currentConfiguration.workerMemoryRequest!))) {
|
||||
this.saveArgs.workerMemoryRequest = undefined;
|
||||
} else if (this.workerMemoryRequestBox.value === '') {
|
||||
this.saveArgs.workerMemoryRequest = '""';
|
||||
} else {
|
||||
this.saveArgs.memoryLimit = this.memoryLimitBox!.value + 'Gi';
|
||||
this.saveArgs.workerMemoryRequest = this.workerMemoryRequestBox.value + 'Gi';
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.memoryRequestBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||
// Worker nodes memory limit
|
||||
this.workerMemoryLimitBox = this.modelView.modelBuilder.inputBox().withProps({
|
||||
readOnly: false,
|
||||
min: 0.25,
|
||||
validationErrorMessage: loc.memoryRequestValidationErrorMessage,
|
||||
inputType: 'number',
|
||||
placeHolder: loc.loading
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.memoryRequestBox.onTextChanged(() => {
|
||||
if (!(this.handleOnTextChanged(this.memoryRequestBox!))) {
|
||||
this.saveArgs.memoryRequest = undefined;
|
||||
this.workerMemoryLimitBox.onTextChanged(() => {
|
||||
if (!(this.saveValueToEdit(this.workerMemoryLimitBox, this.currentConfiguration.workerMemoryLimit!))) {
|
||||
this.saveArgs.workerMemoryLimit = undefined;
|
||||
} else if (this.workerMemoryLimitBox.value === '') {
|
||||
this.saveArgs.workerMemoryLimit = '""';
|
||||
} else {
|
||||
this.saveArgs.memoryRequest = this.memoryRequestBox!.value + 'Gi';
|
||||
this.saveArgs.workerMemoryLimit = this.workerMemoryLimitBox.value + 'Gi';
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Coordinator node cores request
|
||||
this.coordinatorCoresRequestBox = this.modelView.modelBuilder.inputBox().withProps({
|
||||
readOnly: false,
|
||||
min: 1,
|
||||
inputType: 'number',
|
||||
placeHolder: loc.loading
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.coordinatorCoresRequestBox.onTextChanged(() => {
|
||||
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;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Coordinator node cores limit
|
||||
this.coordinatorCoresLimitBox = this.modelView.modelBuilder.inputBox().withProps({
|
||||
readOnly: false,
|
||||
min: 1,
|
||||
inputType: 'number',
|
||||
placeHolder: loc.loading
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.coordinatorCoresLimitBox.onTextChanged(() => {
|
||||
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;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Coordinator node memory request
|
||||
this.coordinatorMemoryRequestBox = this.modelView.modelBuilder.inputBox().withProps({
|
||||
readOnly: false,
|
||||
min: 0.25,
|
||||
inputType: 'number',
|
||||
placeHolder: loc.loading
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.coordinatorMemoryRequestBox.onTextChanged(() => {
|
||||
if (!(this.saveValueToEdit(this.coordinatorMemoryRequestBox, this.currentConfiguration.coordinatorMemoryRequest!))) {
|
||||
this.saveArgs.coordinatorMemoryRequest = undefined;
|
||||
} else if (this.coordinatorMemoryRequestBox.value === '') {
|
||||
this.saveArgs.coordinatorMemoryRequest = '""';
|
||||
} else {
|
||||
this.saveArgs.coordinatorMemoryRequest = this.coordinatorMemoryRequestBox.value + 'Gi';
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Coordinator node memory limit
|
||||
this.coordinatorMemoryLimitBox = this.modelView.modelBuilder.inputBox().withProps({
|
||||
readOnly: false,
|
||||
min: 0.25,
|
||||
inputType: 'number',
|
||||
placeHolder: loc.loading
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.coordinatorMemoryLimitBox.onTextChanged(() => {
|
||||
if (!(this.saveValueToEdit(this.coordinatorMemoryLimitBox, this.currentConfiguration.coordinatorMemoryLimit!))) {
|
||||
this.saveArgs.coordinatorMemoryLimit = undefined;
|
||||
} else if (this.coordinatorMemoryLimitBox.value === '') {
|
||||
this.saveArgs.coordinatorMemoryLimit = '""';
|
||||
} else {
|
||||
this.saveArgs.coordinatorMemoryLimit = this.coordinatorMemoryLimitBox.value + 'Gi';
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private createUserInputSection(): azdata.Component[] {
|
||||
private createUserInputWorkerSection(): azdata.Component[] {
|
||||
if (this._postgresModel.configLastUpdated) {
|
||||
this.editWorkerNodeCount();
|
||||
this.editCores();
|
||||
this.editMemory();
|
||||
this.editWorkerCores();
|
||||
this.editWorkerMemory();
|
||||
}
|
||||
|
||||
return [
|
||||
this.createWorkerNodesSectionContainer(),
|
||||
this.createCoresMemorySection(),
|
||||
this.createConfigurationSectionContainer(loc.coresRequest, this.coresRequestBox!),
|
||||
this.createConfigurationSectionContainer(loc.coresLimit, this.coresLimitBox!),
|
||||
this.createConfigurationSectionContainer(loc.memoryRequest, this.memoryRequestBox!),
|
||||
this.createConfigurationSectionContainer(loc.memoryLimit, this.memoryLimitBox!)
|
||||
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)
|
||||
|
||||
];
|
||||
}
|
||||
@@ -330,7 +462,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
alignItems: 'center'
|
||||
}).component();
|
||||
|
||||
const keyComponent = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
const keyComponent = this.modelView.modelBuilder.text().withProps({
|
||||
value: loc.workerNodeCount,
|
||||
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component();
|
||||
@@ -338,7 +470,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().withProperties<azdata.ButtonProperties>({
|
||||
const information = this.modelView.modelBuilder.button().withProps({
|
||||
iconPath: IconPathHelper.information,
|
||||
title: loc.workerNodesInformation,
|
||||
width: '15px',
|
||||
@@ -350,13 +482,29 @@ 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);
|
||||
|
||||
return flexContainer;
|
||||
}
|
||||
|
||||
private createUserInputCoordinatorSection(): azdata.Component[] {
|
||||
if (this._postgresModel.configLastUpdated) {
|
||||
this.editCoordinatorCores();
|
||||
this.editCoordinatorMemory();
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
private createConfigurationSectionContainer(key: string, input: azdata.Component): azdata.FlexContainer {
|
||||
const inputFlex = { flex: '0 1 150px' };
|
||||
const keyFlex = { flex: `0 1 250px` };
|
||||
@@ -366,7 +514,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
alignItems: 'center'
|
||||
}).component();
|
||||
|
||||
const keyComponent = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
const keyComponent = this.modelView.modelBuilder.text().withProps({
|
||||
value: key,
|
||||
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component();
|
||||
@@ -383,39 +531,43 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
return flexContainer;
|
||||
}
|
||||
|
||||
private handleOnTextChanged(component: azdata.InputBoxComponent): boolean {
|
||||
if ((!component.value)) {
|
||||
// if there is no text found in the inputbox component return false
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
* If value is valid and not equal to original value found in config, add this new value to be considered
|
||||
* for editing and enable save/discard buttons.
|
||||
*
|
||||
* @param component The input box that had an onTextChanged event triggered.
|
||||
* @param originalValue The value that was contained in the input box before user interaction.
|
||||
* @return A boolean that reads true if the new value should be taken in for editing or not.
|
||||
*/
|
||||
private saveValueToEdit(component: azdata.InputBoxComponent, originalValue: string): boolean {
|
||||
if (component.value === originalValue) {
|
||||
return false;
|
||||
} else if ((!component.valid)) {
|
||||
// if value given by user is not valid enable discard button for user
|
||||
// to clear all inputs and return false
|
||||
this.discardButton!.enabled = true;
|
||||
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.saveButton.enabled = true;
|
||||
this.discardButton.enabled = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private editWorkerNodeCount() {
|
||||
private editWorkerNodeCount(): void {
|
||||
// scale.shards was renamed to scale.workers. Check both for backwards compatibility.
|
||||
let scale = this._postgresModel.config?.spec.scale;
|
||||
let currentWorkers = scale?.workers ?? scale?.shards ?? 0;
|
||||
|
||||
this.workerBox!.min = currentWorkers;
|
||||
this.workerBox!.placeHolder = currentWorkers.toString();
|
||||
this.workerBox!.value = '';
|
||||
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.saveArgs.workers = undefined;
|
||||
}
|
||||
|
||||
private createCoresMemorySection(): azdata.DivContainer {
|
||||
private createCoresMemorySection(title: string, description: string): azdata.DivContainer {
|
||||
const titleFlex = { flex: `0 1 250px` };
|
||||
|
||||
const flexContainer = this.modelView.modelBuilder.flexContainer().withLayout({
|
||||
@@ -423,17 +575,17 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
alignItems: 'center'
|
||||
}).component();
|
||||
|
||||
const titleComponent = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: loc.configurationPerNode,
|
||||
const titleComponent = this.modelView.modelBuilder.text().withProps({
|
||||
value: title,
|
||||
CSSStyles: { ...cssStyles.title, 'font-weight': 'bold', 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component();
|
||||
|
||||
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().withProperties<azdata.ButtonProperties>({
|
||||
const information = this.modelView.modelBuilder.button().withProps({
|
||||
iconPath: IconPathHelper.information,
|
||||
title: loc.postgresConfigurationInformation,
|
||||
title: description,
|
||||
width: '15px',
|
||||
height: '15px',
|
||||
enabled: false
|
||||
@@ -448,62 +600,108 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
return configurationSection;
|
||||
}
|
||||
|
||||
private editCores() {
|
||||
let currentCPUSize = this._postgresModel.config?.spec.scheduling?.default?.resources?.requests?.cpu;
|
||||
|
||||
if (!currentCPUSize) {
|
||||
currentCPUSize = '';
|
||||
private editWorkerCores(): void {
|
||||
//Cores Request
|
||||
this.currentConfiguration.workerCoresRequest = this._postgresModel.config?.spec.scheduling?.default?.resources?.requests?.cpu;
|
||||
if (!this.currentConfiguration.workerCoresRequest) {
|
||||
this.currentConfiguration.workerCoresRequest = '';
|
||||
}
|
||||
|
||||
this.coresRequestBox!.validationErrorMessage = loc.validationMin(this.coresRequestBox!.min!);
|
||||
this.coresRequestBox!.placeHolder = currentCPUSize;
|
||||
this.coresRequestBox!.value = '';
|
||||
this.saveArgs.coresRequest = undefined;
|
||||
this.workerCoresRequestBox.placeHolder = '';
|
||||
this.workerCoresRequestBox.value = this.currentConfiguration.workerCoresRequest;
|
||||
this.saveArgs.workerCoresRequest = undefined;
|
||||
|
||||
currentCPUSize = this._postgresModel.config?.spec.scheduling?.default?.resources?.limits?.cpu;
|
||||
|
||||
if (!currentCPUSize) {
|
||||
currentCPUSize = '';
|
||||
// Cores Limit
|
||||
this.currentConfiguration.workerCoresLimit = this._postgresModel.config?.spec.scheduling?.default?.resources?.limits?.cpu;
|
||||
if (!this.currentConfiguration.workerCoresLimit) {
|
||||
this.currentConfiguration.workerCoresLimit = '';
|
||||
}
|
||||
|
||||
this.coresLimitBox!.validationErrorMessage = loc.validationMin(this.coresLimitBox!.min!);
|
||||
this.coresLimitBox!.placeHolder = currentCPUSize;
|
||||
this.coresLimitBox!.value = '';
|
||||
this.saveArgs.coresLimit = undefined;
|
||||
this.workerCoresLimitBox.placeHolder = '';
|
||||
this.workerCoresLimitBox.value = this.currentConfiguration.workerCoresLimit;
|
||||
this.saveArgs.workerCoresLimit = undefined;
|
||||
}
|
||||
|
||||
private editMemory() {
|
||||
let currentMemSizeConversion: string;
|
||||
private editWorkerMemory(): void {
|
||||
//Memory Request
|
||||
let currentMemorySize = this._postgresModel.config?.spec.scheduling?.default?.resources?.requests?.memory;
|
||||
|
||||
if (!currentMemorySize) {
|
||||
currentMemSizeConversion = '';
|
||||
this.currentConfiguration.workerMemoryRequest = '';
|
||||
} else {
|
||||
currentMemSizeConversion = convertToGibibyteString(currentMemorySize);
|
||||
this.currentConfiguration.workerMemoryRequest = convertToGibibyteString(currentMemorySize);
|
||||
}
|
||||
|
||||
this.memoryRequestBox!.placeHolder = currentMemSizeConversion!;
|
||||
this.memoryRequestBox!.value = '';
|
||||
|
||||
this.saveArgs.memoryRequest = undefined;
|
||||
this.workerMemoryRequestBox.placeHolder = '';
|
||||
this.workerMemoryRequestBox.value = this.currentConfiguration.workerMemoryRequest;
|
||||
this.saveArgs.workerMemoryRequest = undefined;
|
||||
|
||||
//Memory Limit
|
||||
currentMemorySize = this._postgresModel.config?.spec.scheduling?.default?.resources?.limits?.memory;
|
||||
|
||||
if (!currentMemorySize) {
|
||||
currentMemSizeConversion = '';
|
||||
this.currentConfiguration.workerMemoryLimit = '';
|
||||
} else {
|
||||
currentMemSizeConversion = convertToGibibyteString(currentMemorySize);
|
||||
this.currentConfiguration.workerMemoryLimit = convertToGibibyteString(currentMemorySize);
|
||||
}
|
||||
|
||||
this.memoryLimitBox!.placeHolder = currentMemSizeConversion!;
|
||||
this.memoryLimitBox!.value = '';
|
||||
|
||||
this.saveArgs.memoryLimit = undefined;
|
||||
this.workerMemoryLimitBox.placeHolder = '';
|
||||
this.workerMemoryLimitBox.value = this.currentConfiguration.workerMemoryLimit;
|
||||
this.saveArgs.workerMemoryLimit = undefined;
|
||||
}
|
||||
|
||||
private handleServiceUpdated() {
|
||||
private editCoordinatorCores(): void {
|
||||
// TODO get current cpu size for coordinator
|
||||
this.currentConfiguration.coordinatorCoresRequest = this._postgresModel.config?.spec.scheduling?.default?.resources?.requests?.cpu;
|
||||
if (!this.currentConfiguration.coordinatorCoresRequest) {
|
||||
this.currentConfiguration.coordinatorCoresRequest = '';
|
||||
}
|
||||
|
||||
this.coordinatorCoresRequestBox.placeHolder = '';
|
||||
this.coordinatorCoresRequestBox.value = this.currentConfiguration.coordinatorCoresRequest;
|
||||
this.saveArgs.coordinatorCoresRequest = undefined;
|
||||
|
||||
// TODO get current cpu size for coordinator
|
||||
this.currentConfiguration.coordinatorCoresLimit = this._postgresModel.config?.spec.scheduling?.default?.resources?.limits?.cpu;
|
||||
if (!this.currentConfiguration.coordinatorCoresLimit) {
|
||||
this.currentConfiguration.coordinatorCoresLimit = '';
|
||||
}
|
||||
|
||||
this.coordinatorCoresLimitBox.placeHolder = '';
|
||||
this.coordinatorCoresLimitBox.value = this.currentConfiguration.coordinatorCoresLimit;
|
||||
this.saveArgs.coordinatorCoresLimit = undefined;
|
||||
}
|
||||
|
||||
private editCoordinatorMemory(): void {
|
||||
// TODO get current memory size for coordinator
|
||||
let currentMemorySize = this._postgresModel.config?.spec.scheduling?.default?.resources?.requests?.memory;
|
||||
if (!currentMemorySize) {
|
||||
this.currentConfiguration.coordinatorCoresRequest = '';
|
||||
} else {
|
||||
this.currentConfiguration.coordinatorCoresRequest = convertToGibibyteString(currentMemorySize);
|
||||
}
|
||||
|
||||
this.coordinatorMemoryRequestBox.placeHolder = '';
|
||||
this.coordinatorMemoryRequestBox.value = this.currentConfiguration.coordinatorMemoryRequest;
|
||||
this.saveArgs.coordinatorMemoryRequest = undefined;
|
||||
|
||||
// TODO get current memory size for coordinator
|
||||
currentMemorySize = this._postgresModel.config?.spec.scheduling?.default?.resources?.limits?.memory;
|
||||
if (!currentMemorySize) {
|
||||
this.currentConfiguration.coordinatorCoresLimit = '';
|
||||
} else {
|
||||
this.currentConfiguration.coordinatorCoresLimit = convertToGibibyteString(currentMemorySize);
|
||||
}
|
||||
|
||||
this.coordinatorMemoryLimitBox.placeHolder = '';
|
||||
this.coordinatorMemoryLimitBox.value = this.currentConfiguration.coordinatorMemoryLimit;
|
||||
this.saveArgs.coordinatorMemoryLimit = undefined;
|
||||
}
|
||||
|
||||
private handleServiceUpdated(): void {
|
||||
this.editWorkerNodeCount();
|
||||
this.editCores();
|
||||
this.editMemory();
|
||||
this.editWorkerCores();
|
||||
this.editWorkerMemory();
|
||||
/* TODO perform once Coordinator section is in view
|
||||
this.editCoordinatorCores();
|
||||
this.editCoordinatorMemory(); */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,10 @@ import { PostgresModel } from '../../../models/postgresModel';
|
||||
|
||||
export class PostgresConnectionStringsPage extends DashboardPage {
|
||||
private keyValueContainer?: KeyValueContainer;
|
||||
private connectionStringsLoading!: azdata.LoadingComponent;
|
||||
|
||||
constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) {
|
||||
super(modelView);
|
||||
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _postgresModel: PostgresModel) {
|
||||
super(modelView, dashboard);
|
||||
|
||||
this.disposables.push(this._postgresModel.onConfigUpdated(
|
||||
() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())));
|
||||
@@ -59,7 +60,14 @@ export class PostgresConnectionStringsPage extends DashboardPage {
|
||||
|
||||
this.keyValueContainer = new KeyValueContainer(this.modelView.modelBuilder, this.getConnectionStrings());
|
||||
this.disposables.push(this.keyValueContainer);
|
||||
content.addItem(this.keyValueContainer.container);
|
||||
|
||||
this.connectionStringsLoading = this.modelView.modelBuilder.loadingComponent()
|
||||
.withItem(this.keyValueContainer.container)
|
||||
.withProperties<azdata.LoadingComponentProperties>({
|
||||
loading: !this._postgresModel.configLastUpdated
|
||||
}).component();
|
||||
|
||||
content.addItem(this.connectionStringsLoading, { CSSStyles: cssStyles.text });
|
||||
this.initialized = true;
|
||||
return root;
|
||||
}
|
||||
@@ -88,5 +96,6 @@ export class PostgresConnectionStringsPage extends DashboardPage {
|
||||
|
||||
private handleServiceUpdated() {
|
||||
this.keyValueContainer?.refresh(this.getConnectionStrings());
|
||||
this.connectionStringsLoading.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { IconPathHelper } from '../../../constants';
|
||||
import { PostgresParametersPage } from './postgresParameters';
|
||||
import { PostgresModel, EngineSettingsModel } from '../../../models/postgresModel';
|
||||
|
||||
export class PostgresCoordinatorNodeParametersPage extends PostgresParametersPage {
|
||||
|
||||
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, postgresModel: PostgresModel) {
|
||||
super(modelView, dashboard, postgresModel);
|
||||
}
|
||||
|
||||
protected get title(): string {
|
||||
return loc.coordinatorNodeParameters;
|
||||
}
|
||||
|
||||
protected get id(): string {
|
||||
return 'postgres-coordinator-node-parameters';
|
||||
}
|
||||
|
||||
protected get icon(): { dark: string; light: string; } {
|
||||
return IconPathHelper.gearGray;
|
||||
}
|
||||
|
||||
protected get description(): string {
|
||||
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.controllerModel.azdataAdditionalEnvVars,
|
||||
session);
|
||||
*/
|
||||
}
|
||||
|
||||
protected async resetAllParameters(): Promise<void> {
|
||||
/* TODO add correct azdata call for editing coordinator parameters
|
||||
await this._azdataApi.azdata.arc.postgres.server.edit(
|
||||
this._postgresModel.info.name,
|
||||
{ engineSettings: `''`, replaceEngineSettings: true },
|
||||
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
|
||||
session);
|
||||
*/
|
||||
}
|
||||
|
||||
protected async resetParameter(): Promise<void> {
|
||||
/* TODO add correct azdata call for editing coordinator parameters
|
||||
await this._azdataApi.azdata.arc.postgres.server.edit(
|
||||
this._postgresModel.info.name,
|
||||
{ engineSettings: parameterName + '=' },
|
||||
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
|
||||
session);
|
||||
*/
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,9 @@ import { Dashboard } from '../../components/dashboard';
|
||||
import { PostgresDiagnoseAndSolveProblemsPage } from './postgresDiagnoseAndSolveProblemsPage';
|
||||
import { PostgresSupportRequestPage } from './postgresSupportRequestPage';
|
||||
import { PostgresComputeAndStoragePage } from './postgresComputeAndStoragePage';
|
||||
import { PostgresParametersPage } from './postgresParametersPage';
|
||||
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) {
|
||||
@@ -31,13 +32,16 @@ export class PostgresDashboard extends Dashboard {
|
||||
}
|
||||
|
||||
protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> {
|
||||
const overviewPage = new PostgresOverviewPage(modelView, this._controllerModel, this._postgresModel);
|
||||
const connectionStringsPage = new PostgresConnectionStringsPage(modelView, this._postgresModel);
|
||||
const computeAndStoragePage = new PostgresComputeAndStoragePage(modelView, this._postgresModel);
|
||||
const propertiesPage = new PostgresPropertiesPage(modelView, this._controllerModel, this._postgresModel);
|
||||
const parametersPage = new PostgresParametersPage(modelView, this._postgresModel);
|
||||
const diagnoseAndSolveProblemsPage = new PostgresDiagnoseAndSolveProblemsPage(modelView, this._context, this._postgresModel);
|
||||
const supportRequestPage = new PostgresSupportRequestPage(modelView, this._controllerModel, this._postgresModel);
|
||||
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);
|
||||
// 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);
|
||||
|
||||
return [
|
||||
overviewPage.tab,
|
||||
@@ -47,12 +51,13 @@ export class PostgresDashboard extends Dashboard {
|
||||
propertiesPage.tab,
|
||||
connectionStringsPage.tab,
|
||||
computeAndStoragePage.tab,
|
||||
parametersPage.tab
|
||||
workerNodeParametersPage.tab
|
||||
]
|
||||
},
|
||||
{
|
||||
title: loc.supportAndTroubleshooting,
|
||||
tabs: [
|
||||
resourceHealthPage.tab,
|
||||
diagnoseAndSolveProblemsPage.tab,
|
||||
supportRequestPage.tab
|
||||
]
|
||||
|
||||
@@ -9,10 +9,11 @@ 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(protected modelView: azdata.ModelView, private _context: vscode.ExtensionContext, private _postgresModel: PostgresModel) {
|
||||
super(modelView);
|
||||
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _context: vscode.ExtensionContext, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
|
||||
super(modelView, dashboard);
|
||||
}
|
||||
|
||||
protected get title(): string {
|
||||
@@ -50,9 +51,8 @@ export class PostgresDiagnoseAndSolveProblemsPage extends DashboardPage {
|
||||
|
||||
this.disposables.push(
|
||||
troubleshootButton.onDidClick(() => {
|
||||
process.env['POSTGRES_SERVER_NAMESPACE'] = this._postgresModel.config?.metadata.namespace;
|
||||
process.env['POSTGRES_SERVER_NAMESPACE'] = this._controllerModel.controllerConfig?.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');
|
||||
}));
|
||||
|
||||
|
||||
@@ -7,27 +7,38 @@ import * as vscode from 'vscode';
|
||||
import * as azdata from 'azdata';
|
||||
import * as azdataExt from 'azdata-ext';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { IconPathHelper, cssStyles } from '../../../constants';
|
||||
import { IconPathHelper, cssStyles, iconSize } from '../../../constants';
|
||||
import { DashboardPage } from '../../components/dashboardPage';
|
||||
import { ControllerModel } from '../../../models/controllerModel';
|
||||
import { PostgresModel } from '../../../models/postgresModel';
|
||||
import { promptAndConfirmPassword, promptForInstanceDeletion } from '../../../common/utils';
|
||||
import { ResourceType } from 'arc';
|
||||
|
||||
export type PodStatusModel = {
|
||||
podName: azdata.Component,
|
||||
type: string,
|
||||
status: string
|
||||
};
|
||||
|
||||
export class PostgresOverviewPage extends DashboardPage {
|
||||
|
||||
private propertiesLoading!: azdata.LoadingComponent;
|
||||
private serverGroupNodesLoading!: azdata.LoadingComponent;
|
||||
private kibanaLoading!: azdata.LoadingComponent;
|
||||
private grafanaLoading!: azdata.LoadingComponent;
|
||||
|
||||
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, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
|
||||
super(modelView);
|
||||
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
|
||||
super(modelView, dashboard);
|
||||
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
|
||||
|
||||
this.disposables.push(
|
||||
@@ -132,8 +143,63 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
[loc.kibanaDashboard, this.kibanaLoading, loc.kibanaDashboardDescription],
|
||||
[loc.grafanaDashboard, this.grafanaLoading, loc.grafanaDashboardDescription]]
|
||||
}).component();
|
||||
|
||||
content.addItem(endpointsTable);
|
||||
|
||||
// Server Group Nodes
|
||||
content.addItem(this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: loc.serverGroupNodes,
|
||||
CSSStyles: titleCSS
|
||||
}).component());
|
||||
|
||||
this.podStatusTable = this.modelView.modelBuilder.declarativeTable().withProps({
|
||||
width: '100%',
|
||||
columns: [
|
||||
{
|
||||
displayName: loc.name,
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
isReadOnly: true,
|
||||
width: '35%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: {
|
||||
...cssStyles.tableRow,
|
||||
'overflow': 'hidden',
|
||||
'text-overflow': 'ellipsis',
|
||||
'white-space': 'nowrap',
|
||||
'max-width': '0'
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: loc.type,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: '35%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
},
|
||||
{
|
||||
displayName: loc.status,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: '30%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
}
|
||||
],
|
||||
data: [this.podStatusData.map(p => [p.podName, p.type, p.status])]
|
||||
}).component();
|
||||
|
||||
|
||||
|
||||
this.serverGroupNodesLoading = this.modelView.modelBuilder.loadingComponent()
|
||||
.withItem(this.podStatusTable)
|
||||
.withProperties<azdata.LoadingComponentProperties>({
|
||||
loading: !this._postgresModel.configLastUpdated
|
||||
}).component();
|
||||
|
||||
this.refreshServerNodes();
|
||||
|
||||
content.addItem(this.serverGroupNodesLoading, { CSSStyles: cssStyles.text });
|
||||
|
||||
this.initialized = true;
|
||||
return root;
|
||||
}
|
||||
@@ -151,21 +217,13 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
try {
|
||||
const password = await promptAndConfirmPassword(input => !input ? loc.enterANonEmptyPassword : '');
|
||||
if (password) {
|
||||
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();
|
||||
}
|
||||
await this._azdataApi.azdata.arc.postgres.server.edit(
|
||||
this._postgresModel.info.name,
|
||||
{
|
||||
adminPassword: true,
|
||||
noWait: true
|
||||
},
|
||||
Object.assign({ 'AZDATA_PASSWORD': password }, this._controllerModel.azdataAdditionalEnvVars));
|
||||
vscode.window.showInformationMessage(loc.passwordReset);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -176,14 +234,14 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
}));
|
||||
|
||||
// Delete service
|
||||
const deleteButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
|
||||
this.deleteButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
|
||||
label: loc.deleteText,
|
||||
iconPath: IconPathHelper.delete
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
deleteButton.onDidClick(async () => {
|
||||
deleteButton.enabled = false;
|
||||
this.deleteButton.onDidClick(async () => {
|
||||
this.deleteButton.enabled = false;
|
||||
try {
|
||||
if (await promptForInstanceDeletion(this._postgresModel.info.name)) {
|
||||
await vscode.window.withProgress(
|
||||
@@ -193,22 +251,23 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
cancellable: false
|
||||
},
|
||||
async (_progress, _token) => {
|
||||
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();
|
||||
}
|
||||
|
||||
return await this._azdataApi.azdata.arc.postgres.server.delete(this._postgresModel.info.name, this._controllerModel.azdataAdditionalEnvVars, this._controllerModel.controllerContext);
|
||||
}
|
||||
);
|
||||
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 {
|
||||
deleteButton.enabled = true;
|
||||
this.deleteButton.enabled = true;
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -223,6 +282,7 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
refreshButton.enabled = false;
|
||||
try {
|
||||
this.propertiesLoading!.loading = true;
|
||||
this.serverGroupNodesLoading!.loading = true;
|
||||
this.kibanaLoading!.loading = true;
|
||||
this.grafanaLoading!.loading = true;
|
||||
|
||||
@@ -257,7 +317,7 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
|
||||
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([
|
||||
{ component: resetPasswordButton },
|
||||
{ component: deleteButton },
|
||||
{ component: this.deleteButton },
|
||||
{ component: refreshButton, toolbarSeparatorAfter: true },
|
||||
{ component: openInAzurePortalButton }
|
||||
]).component();
|
||||
@@ -281,6 +341,54 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
];
|
||||
}
|
||||
|
||||
private getPodStatus(): PodStatusModel[] {
|
||||
let podModels: PodStatusModel[] = [];
|
||||
const podStatus = this._postgresModel.config?.status.podsStatus;
|
||||
|
||||
podStatus?.forEach(p => {
|
||||
// If a condition of the pod has a status of False, pod is not Ready
|
||||
const status = p.conditions.find(c => c.status === 'False') ? loc.notReady : loc.ready;
|
||||
|
||||
const podLabelContainer = this.modelView.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: { 'alignItems': 'center', 'height': '15px' }
|
||||
}).component();
|
||||
|
||||
const imageComponent = this.modelView.modelBuilder.image().withProps({
|
||||
iconPath: IconPathHelper.postgres,
|
||||
width: iconSize,
|
||||
height: iconSize,
|
||||
iconHeight: '15px',
|
||||
iconWidth: '15px'
|
||||
}).component();
|
||||
|
||||
let podLabel = this.modelView.modelBuilder.text().withProps({
|
||||
value: p.name,
|
||||
}).component();
|
||||
|
||||
if (p.role.toUpperCase() === loc.worker.toUpperCase()) {
|
||||
podLabelContainer.addItem(imageComponent, { CSSStyles: { 'margin-left': '15px', 'margin-right': '0px' } });
|
||||
podLabelContainer.addItem(podLabel);
|
||||
let pod: PodStatusModel = {
|
||||
podName: podLabelContainer,
|
||||
type: loc.worker,
|
||||
status: status
|
||||
};
|
||||
podModels.push(pod);
|
||||
} else {
|
||||
podLabelContainer.addItem(imageComponent, { CSSStyles: { 'margin-right': '0px' } });
|
||||
podLabelContainer.addItem(podLabel);
|
||||
let pod: PodStatusModel = {
|
||||
podName: podLabelContainer,
|
||||
type: loc.coordinator,
|
||||
status: status
|
||||
};
|
||||
podModels.unshift(pod);
|
||||
}
|
||||
});
|
||||
|
||||
return podModels;
|
||||
}
|
||||
|
||||
private refreshDashboardLinks(): void {
|
||||
if (this._postgresModel.config) {
|
||||
const kibanaUrl = this._postgresModel.config.status.logSearchDashboard ?? '';
|
||||
@@ -295,6 +403,14 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
}
|
||||
}
|
||||
|
||||
private refreshServerNodes(): void {
|
||||
if (this._postgresModel.config) {
|
||||
this.podStatusData = this.getPodStatus();
|
||||
this.podStatusTable.data = this.podStatusData.map(p => [p.podName, p.type, p.status]);
|
||||
this.serverGroupNodesLoading.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private handleRegistrationsUpdated() {
|
||||
this.properties!.propertyItems = this.getProperties();
|
||||
this.propertiesLoading!.loading = false;
|
||||
@@ -304,5 +420,6 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
this.properties!.propertyItems = this.getProperties();
|
||||
this.propertiesLoading!.loading = false;
|
||||
this.refreshDashboardLinks();
|
||||
this.refreshServerNodes();
|
||||
}
|
||||
}
|
||||
|
||||
626
extensions/arc/src/ui/dashboards/postgres/postgresParameters.ts
Normal file
626
extensions/arc/src/ui/dashboards/postgres/postgresParameters.ts
Normal file
@@ -0,0 +1,626 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 azdataExt from 'azdata-ext';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { UserCancelledError } from '../../../common/api';
|
||||
import { IconPathHelper, cssStyles } from '../../../constants';
|
||||
import { DashboardPage } from '../../components/dashboardPage';
|
||||
import { EngineSettingsModel, PostgresModel } from '../../../models/postgresModel';
|
||||
import { debounce } from '../../../common/utils';
|
||||
|
||||
export type ParametersModel = {
|
||||
parameterName: string,
|
||||
originalValue: string,
|
||||
valueComponent: azdata.TextComponent | azdata.DropDownComponent | azdata.CheckBoxComponent,
|
||||
information?: azdata.ButtonComponent,
|
||||
description: string,
|
||||
resetButton: azdata.ButtonComponent
|
||||
};
|
||||
|
||||
export abstract class PostgresParametersPage extends DashboardPage {
|
||||
private searchBox!: azdata.InputBoxComponent;
|
||||
protected _parametersTable!: azdata.DeclarativeTableComponent;
|
||||
private parameterContainer!: azdata.DivContainer;
|
||||
private parametersTableLoading?: azdata.LoadingComponent;
|
||||
|
||||
private discardButton!: azdata.ButtonComponent;
|
||||
private saveButton!: azdata.ButtonComponent;
|
||||
private resetAllButton!: azdata.ButtonComponent;
|
||||
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);
|
||||
|
||||
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
|
||||
|
||||
this.initializeSearchBox();
|
||||
|
||||
this.disposables.push(
|
||||
this._postgresModel.onConfigUpdated(() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())),
|
||||
this._postgresModel.onEngineSettingsUpdated(() => this.eventuallyRunOnInitialized(() => this.refreshParametersTable()))
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
root.addItem(content, { CSSStyles: { 'margin': '20px' } });
|
||||
|
||||
content.addItem(this.modelView.modelBuilder.text().withProps({
|
||||
value: this.title,
|
||||
CSSStyles: { ...cssStyles.title }
|
||||
}).component());
|
||||
|
||||
content.addItem(this.modelView.modelBuilder.text().withProps({
|
||||
value: this.description,
|
||||
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component());
|
||||
|
||||
content.addItem(this.modelView.modelBuilder.hyperlink().withProps({
|
||||
label: loc.learnAboutNodeParameters,
|
||||
url: 'https://docs.microsoft.com/azure/azure-arc/data/configure-server-parameters-postgresql-hyperscale'
|
||||
}).component(), { CSSStyles: { 'margin-bottom': '20px' } });
|
||||
|
||||
content.addItem(this.searchBox!, { CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px', 'margin-bottom': '20px' } });
|
||||
|
||||
this._parametersTable = this.modelView.modelBuilder.declarativeTable().withProps({
|
||||
width: '100%',
|
||||
columns: [
|
||||
{
|
||||
displayName: loc.parameterName,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: '20%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
},
|
||||
{
|
||||
displayName: loc.value,
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
isReadOnly: false,
|
||||
width: '20%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
},
|
||||
{
|
||||
displayName: loc.description,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: '50%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: {
|
||||
...cssStyles.tableRow
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: loc.resetToDefault,
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
isReadOnly: false,
|
||||
width: '10%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
}
|
||||
],
|
||||
data: []
|
||||
}).component();
|
||||
|
||||
this.parameterContainer = this.modelView.modelBuilder.divContainer().component();
|
||||
this.selectComponent();
|
||||
content.addItem(this.parameterContainer);
|
||||
|
||||
this.initialized = true;
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
protected get toolbarContainer(): azdata.ToolbarContainer {
|
||||
// Save Edits
|
||||
this.saveButton = this.modelView.modelBuilder.button().withProps({
|
||||
label: loc.saveText,
|
||||
iconPath: IconPathHelper.save,
|
||||
enabled: false
|
||||
}).component();
|
||||
|
||||
let engineSettings: string[] = [];
|
||||
this.disposables.push(
|
||||
this.saveButton.onDidClick(async () => {
|
||||
this.saveButton.enabled = false;
|
||||
try {
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: loc.updatingInstance(this._postgresModel.info.name),
|
||||
cancellable: false
|
||||
},
|
||||
async (_progress, _token): Promise<void> => {
|
||||
try {
|
||||
this.parameterUpdates.forEach((value, key) => {
|
||||
engineSettings.push(`${key}="${value}"`);
|
||||
});
|
||||
await this.saveParameterEdits(engineSettings.toString());
|
||||
} 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;
|
||||
throw err;
|
||||
}
|
||||
try {
|
||||
await this._postgresModel.refresh();
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.refreshFailed(error));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
vscode.window.showInformationMessage(loc.instanceUpdated(this._postgresModel.info.name));
|
||||
|
||||
engineSettings = [];
|
||||
this.changedComponentValues.clear();
|
||||
this.parameterUpdates.clear();
|
||||
this.discardButton.enabled = false;
|
||||
this.resetAllButton.enabled = true;
|
||||
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.instanceUpdateFailed(this._postgresModel.info.name, error));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Discard
|
||||
this.discardButton = this.modelView.modelBuilder.button().withProps({
|
||||
label: loc.discardText,
|
||||
iconPath: IconPathHelper.discard,
|
||||
enabled: false
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.discardButton.onDidClick(async () => {
|
||||
this.discardButton.enabled = false;
|
||||
try {
|
||||
this.discardParametersTableChanges();
|
||||
} catch (error) {
|
||||
this.discardButton!.enabled = true;
|
||||
vscode.window.showErrorMessage(loc.pageDiscardFailed(error));
|
||||
} finally {
|
||||
this.changedComponentValues.clear();
|
||||
this.saveButton.enabled = false;
|
||||
this.parameterUpdates.clear();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Reset all
|
||||
this.resetAllButton = this.modelView.modelBuilder.button().withProps({
|
||||
label: loc.resetAllToDefault,
|
||||
iconPath: IconPathHelper.reset,
|
||||
enabled: false
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.resetAllButton.onDidClick(async () => {
|
||||
this.resetAllButton.enabled = false;
|
||||
this.discardButton.enabled = false;
|
||||
this.saveButton.enabled = false;
|
||||
try {
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: loc.updatingInstance(this._postgresModel.info.name),
|
||||
cancellable: false
|
||||
},
|
||||
async (_progress, _token): Promise<void> => {
|
||||
try {
|
||||
await this.resetAllParameters();
|
||||
} 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.resetAllButton.enabled = true;
|
||||
throw err;
|
||||
}
|
||||
this.changedComponentValues.clear();
|
||||
try {
|
||||
await this._postgresModel.refresh();
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.refreshFailed(error));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
vscode.window.showInformationMessage(loc.instanceUpdated(this._postgresModel.info.name));
|
||||
this.parameterUpdates.clear();
|
||||
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.resetFailed(error));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([
|
||||
{ component: this.saveButton },
|
||||
{ component: this.discardButton },
|
||||
{ component: this.resetAllButton }
|
||||
]).component();
|
||||
}
|
||||
|
||||
protected initializeConnectButton(): void {
|
||||
this.connectToServerButton = this.modelView.modelBuilder.button().withProps({
|
||||
label: loc.connectToServer,
|
||||
enabled: false,
|
||||
CSSStyles: { 'max-width': '125px' }
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
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);
|
||||
if (response !== loc.yes) {
|
||||
this.connectToServerButton!.enabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: loc.installingExtension(loc.postgresExtension),
|
||||
cancellable: false
|
||||
},
|
||||
async (_progress, _token): Promise<void> => {
|
||||
try {
|
||||
await vscode.commands.executeCommand('workbench.extensions.installExtension', loc.postgresExtension);
|
||||
} catch (err) {
|
||||
vscode.window.showErrorMessage(loc.extensionInstallationFailed(loc.postgresExtension));
|
||||
this.connectToServerButton!.enabled = true;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
);
|
||||
vscode.window.showInformationMessage(loc.extensionInstalled(loc.postgresExtension));
|
||||
}
|
||||
|
||||
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);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private selectComponent(): void {
|
||||
if (!this._postgresModel.engineSettingsLastUpdated) {
|
||||
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);
|
||||
} else {
|
||||
this.searchBox.enabled = true;
|
||||
this.resetAllButton.enabled = true;
|
||||
this.parameterContainer.addItem(this._parametersTable!);
|
||||
this.refreshParametersTable();
|
||||
}
|
||||
}
|
||||
|
||||
private async callGetEngineSettings(): Promise<void> {
|
||||
try {
|
||||
await this._postgresModel.getEngineSettings();
|
||||
} catch (error) {
|
||||
if (error instanceof UserCancelledError) {
|
||||
vscode.window.showWarningMessage(loc.pgConnectionRequired);
|
||||
} else {
|
||||
vscode.window.showErrorMessage(loc.fetchEngineSettingsFailed(this._postgresModel.info.name, error));
|
||||
}
|
||||
this.connectToServerButton!.enabled = true;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
protected initializeSearchBox(): void {
|
||||
this.searchBox = this.modelView.modelBuilder.inputBox().withProps({
|
||||
readOnly: false,
|
||||
enabled: false,
|
||||
placeHolder: loc.searchToFilter
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.searchBox.onTextChanged(() => {
|
||||
this.onSearchFilter();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@debounce(500)
|
||||
private onSearchFilter(): void {
|
||||
if (!this.searchBox.value) {
|
||||
this._parametersTable.setFilter(undefined);
|
||||
} else {
|
||||
this.filterParameters(this.searchBox.value);
|
||||
}
|
||||
}
|
||||
|
||||
private filterParameters(search: string): void {
|
||||
const filteredRowIndexes: number[] = [];
|
||||
this._parametersTable.data?.forEach((row, index) => {
|
||||
if (row[0].toUpperCase()?.search(search.toUpperCase()) !== -1 || row[2].toUpperCase()?.search(search.toUpperCase()) !== -1) {
|
||||
filteredRowIndexes.push(index);
|
||||
}
|
||||
});
|
||||
this._parametersTable.setFilter(filteredRowIndexes);
|
||||
}
|
||||
|
||||
private handleOnTextChanged(component: azdata.InputBoxComponent, name: string, currentValue: string | undefined): boolean {
|
||||
if (!component.valid) {
|
||||
// If invalid value return false and enable discard button
|
||||
this.discardButton.enabled = true;
|
||||
this.collectChangedComponents(name);
|
||||
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);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
protected createParameterComponents(engineSetting: EngineSettingsModel): ParametersModel {
|
||||
|
||||
// Can reset individual parameter
|
||||
const resetParameterButton = this.modelView.modelBuilder.button().withProps({
|
||||
iconPath: IconPathHelper.reset,
|
||||
title: loc.resetToDefault,
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
enabled: true
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
resetParameterButton.onDidClick(async () => {
|
||||
try {
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: loc.updatingInstance(this._postgresModel.info.name),
|
||||
cancellable: false
|
||||
},
|
||||
async (_progress, _token): Promise<void> => {
|
||||
await this.resetParameter(engineSetting.parameterName!);
|
||||
try {
|
||||
await this._postgresModel.refresh();
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.refreshFailed(error));
|
||||
}
|
||||
}
|
||||
);
|
||||
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));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
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 {
|
||||
parameterName: engineSetting.parameterName!,
|
||||
originalValue: engineSetting.value!,
|
||||
valueComponent: valueComponent,
|
||||
description: engineSetting.description!,
|
||||
resetButton: resetParameterButton
|
||||
};
|
||||
}
|
||||
|
||||
private collectChangedComponents(name: string): void {
|
||||
if (!this.changedComponentValues.has(name)) {
|
||||
this.changedComponentValues.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
private removeFromChangedComponents(name: string): void {
|
||||
if (this.changedComponentValues.has(name)) {
|
||||
this.changedComponentValues.delete(name);
|
||||
}
|
||||
}
|
||||
|
||||
private discardParametersTableChanges(): void {
|
||||
let instanceOfCheckBox = function (object: any): object is azdata.CheckBoxComponent {
|
||||
return 'checked' in object;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract saveParameterEdits(engineSettings: string): Promise<void>;
|
||||
|
||||
protected abstract resetAllParameters(): Promise<void>;
|
||||
|
||||
protected abstract resetParameter(parameterName: string): Promise<void>;
|
||||
}
|
||||
@@ -1,588 +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 azdataExt from 'azdata-ext';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { UserCancelledError } from '../../../common/api';
|
||||
import { IconPathHelper, cssStyles } from '../../../constants';
|
||||
import { DashboardPage } from '../../components/dashboardPage';
|
||||
import { EngineSettingsModel, PostgresModel } from '../../../models/postgresModel';
|
||||
import { debounce } from '../../../common/utils';
|
||||
|
||||
export type ParametersModel = {
|
||||
parameterName: string,
|
||||
valueContainer: azdata.FlexContainer,
|
||||
description: string,
|
||||
resetButton: azdata.ButtonComponent
|
||||
};
|
||||
|
||||
export class PostgresParametersPage extends DashboardPage {
|
||||
private searchBox!: azdata.InputBoxComponent;
|
||||
private parametersTable!: azdata.DeclarativeTableComponent;
|
||||
private parameterContainer?: azdata.DivContainer;
|
||||
private _parametersTableLoading!: azdata.LoadingComponent;
|
||||
|
||||
private discardButton!: azdata.ButtonComponent;
|
||||
private saveButton!: azdata.ButtonComponent;
|
||||
private resetAllButton!: azdata.ButtonComponent;
|
||||
private connectToServerButton?: azdata.ButtonComponent;
|
||||
|
||||
private _parameters: ParametersModel[] = [];
|
||||
private parameterUpdates: Map<string, string> = new Map();
|
||||
|
||||
private readonly _azdataApi: azdataExt.IExtension;
|
||||
|
||||
constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) {
|
||||
super(modelView);
|
||||
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
|
||||
|
||||
this.initializeConnectButton();
|
||||
this.initializeSearchBox();
|
||||
|
||||
this.disposables.push(
|
||||
this._postgresModel.onConfigUpdated(() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())),
|
||||
this._postgresModel.onEngineSettingsUpdated(() => this.eventuallyRunOnInitialized(() => this.refreshParametersTable()))
|
||||
);
|
||||
}
|
||||
|
||||
protected get title(): string {
|
||||
return loc.nodeParameters;
|
||||
}
|
||||
|
||||
protected get id(): string {
|
||||
return 'postgres-node-parameters';
|
||||
}
|
||||
|
||||
protected get icon(): { dark: string; light: string; } {
|
||||
return IconPathHelper.gear;
|
||||
}
|
||||
|
||||
protected get container(): azdata.Component {
|
||||
const root = this.modelView.modelBuilder.divContainer().component();
|
||||
const content = this.modelView.modelBuilder.divContainer().component();
|
||||
root.addItem(content, { CSSStyles: { 'margin': '20px' } });
|
||||
|
||||
content.addItem(this.modelView.modelBuilder.text().withProps({
|
||||
value: loc.nodeParameters,
|
||||
CSSStyles: { ...cssStyles.title }
|
||||
}).component());
|
||||
|
||||
content.addItem(this.modelView.modelBuilder.text().withProps({
|
||||
value: loc.nodeParametersDescription,
|
||||
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component());
|
||||
|
||||
content.addItem(this.modelView.modelBuilder.hyperlink().withProps({
|
||||
label: loc.learnAboutNodeParameters,
|
||||
url: 'https://docs.microsoft.com/azure/azure-arc/data/configure-server-parameters-postgresql-hyperscale'
|
||||
}).component(), { CSSStyles: { 'margin-bottom': '20px' } });
|
||||
|
||||
content.addItem(this.searchBox!, { CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px', 'margin-bottom': '20px' } });
|
||||
|
||||
this.parametersTable = this.modelView.modelBuilder.declarativeTable().withProps({
|
||||
width: '100%',
|
||||
columns: [
|
||||
{
|
||||
displayName: loc.parameterName,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: '20%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
},
|
||||
{
|
||||
displayName: loc.value,
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
isReadOnly: false,
|
||||
width: '20%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
},
|
||||
{
|
||||
displayName: loc.description,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: '50%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: {
|
||||
...cssStyles.tableRow
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: loc.resetToDefault,
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
isReadOnly: false,
|
||||
width: '10%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
}
|
||||
],
|
||||
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;
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
protected get toolbarContainer(): azdata.ToolbarContainer {
|
||||
// Save Edits
|
||||
this.saveButton = this.modelView.modelBuilder.button().withProps({
|
||||
label: loc.saveText,
|
||||
iconPath: IconPathHelper.save,
|
||||
enabled: false
|
||||
}).component();
|
||||
|
||||
let engineSettings: string[] = [];
|
||||
this.disposables.push(
|
||||
this.saveButton.onDidClick(async () => {
|
||||
this.saveButton!.enabled = false;
|
||||
try {
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: loc.updatingInstance(this._postgresModel.info.name),
|
||||
cancellable: false
|
||||
},
|
||||
async (_progress, _token): Promise<void> => {
|
||||
try {
|
||||
this.parameterUpdates!.forEach((value, key) => {
|
||||
engineSettings.push(`${key}="${value}"`);
|
||||
});
|
||||
const session = await this._postgresModel.controllerModel.acquireAzdataSession();
|
||||
try {
|
||||
await this._azdataApi.azdata.arc.postgres.server.edit(
|
||||
this._postgresModel.info.name,
|
||||
{ engineSettings: engineSettings.toString() },
|
||||
this._postgresModel.engineVersion,
|
||||
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
|
||||
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;
|
||||
throw err;
|
||||
}
|
||||
await this._postgresModel.refresh();
|
||||
}
|
||||
);
|
||||
|
||||
vscode.window.showInformationMessage(loc.instanceUpdated(this._postgresModel.info.name));
|
||||
|
||||
engineSettings = [];
|
||||
this.parameterUpdates!.clear();
|
||||
this.discardButton!.enabled = false;
|
||||
this.resetAllButton!.enabled = true;
|
||||
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.instanceUpdateFailed(this._postgresModel.info.name, error));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Discard
|
||||
this.discardButton = this.modelView.modelBuilder.button().withProps({
|
||||
label: loc.discardText,
|
||||
iconPath: IconPathHelper.discard,
|
||||
enabled: false
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.discardButton.onDidClick(async () => {
|
||||
this.discardButton!.enabled = false;
|
||||
try {
|
||||
this.refreshParametersTable();
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.pageDiscardFailed(error));
|
||||
} finally {
|
||||
this.saveButton!.enabled = false;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Reset all
|
||||
this.resetAllButton = this.modelView.modelBuilder.button().withProps({
|
||||
label: loc.resetAllToDefault,
|
||||
iconPath: IconPathHelper.reset,
|
||||
enabled: false
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.resetAllButton.onDidClick(async () => {
|
||||
this.resetAllButton!.enabled = false;
|
||||
this.discardButton!.enabled = false;
|
||||
this.saveButton!.enabled = false;
|
||||
try {
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: loc.updatingInstance(this._postgresModel.info.name),
|
||||
cancellable: false
|
||||
},
|
||||
async (_progress, _token): Promise<void> => {
|
||||
//all
|
||||
// azdata arc postgres server edit -n <server group name> -e '' -re
|
||||
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,
|
||||
{ engineSettings: `''`, replaceEngineSettings: true },
|
||||
this._postgresModel.engineVersion,
|
||||
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
|
||||
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.resetAllButton!.enabled = true;
|
||||
throw err;
|
||||
} finally {
|
||||
session?.dispose();
|
||||
}
|
||||
await this._postgresModel.refresh();
|
||||
}
|
||||
|
||||
);
|
||||
|
||||
vscode.window.showInformationMessage(loc.instanceUpdated(this._postgresModel.info.name));
|
||||
this.parameterUpdates!.clear();
|
||||
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.resetFailed(error));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([
|
||||
{ component: this.saveButton },
|
||||
{ component: this.discardButton },
|
||||
{ component: this.resetAllButton }
|
||||
]).component();
|
||||
}
|
||||
|
||||
private initializeConnectButton(): void {
|
||||
this.connectToServerButton = this.modelView.modelBuilder.button().withProps({
|
||||
label: loc.connectToServer,
|
||||
enabled: false,
|
||||
CSSStyles: { 'max-width': '125px' }
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
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);
|
||||
if (response !== loc.yes) {
|
||||
this.connectToServerButton!.enabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: loc.installingExtension(loc.postgresExtension),
|
||||
cancellable: false
|
||||
},
|
||||
async (_progress, _token): Promise<void> => {
|
||||
try {
|
||||
await vscode.commands.executeCommand('workbench.extensions.installExtension', loc.postgresExtension);
|
||||
} catch (err) {
|
||||
vscode.window.showErrorMessage(loc.extensionInstallationFailed(loc.postgresExtension));
|
||||
this.connectToServerButton!.enabled = true;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
);
|
||||
vscode.window.showInformationMessage(loc.extensionInstalled(loc.postgresExtension));
|
||||
}
|
||||
|
||||
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);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private selectComponent(): void {
|
||||
if (!this._postgresModel.engineSettingsLastUpdated) {
|
||||
this.parameterContainer!.addItem(this.modelView.modelBuilder.text().withProps({
|
||||
value: loc.connectToPostgresDescription,
|
||||
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component());
|
||||
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.refreshParametersTable();
|
||||
}
|
||||
}
|
||||
|
||||
private async callGetEngineSettings(): Promise<void> {
|
||||
try {
|
||||
await this._postgresModel.getEngineSettings();
|
||||
} catch (error) {
|
||||
if (error instanceof UserCancelledError) {
|
||||
vscode.window.showWarningMessage(loc.pgConnectionRequired);
|
||||
} else {
|
||||
vscode.window.showErrorMessage(loc.fetchEngineSettingsFailed(this._postgresModel.info.name, error));
|
||||
}
|
||||
this.connectToServerButton!.enabled = true;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private initializeSearchBox(): void {
|
||||
this.searchBox = this.modelView.modelBuilder.inputBox().withProps({
|
||||
readOnly: false,
|
||||
enabled: false,
|
||||
placeHolder: loc.searchToFilter
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.searchBox.onTextChanged(() => {
|
||||
this.onSearchFilter();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@debounce(500)
|
||||
private onSearchFilter(): void {
|
||||
if (!this.searchBox!.value) {
|
||||
this.parametersTable.setFilter(undefined);
|
||||
} else {
|
||||
this.filterParameters(this.searchBox!.value);
|
||||
}
|
||||
}
|
||||
|
||||
private filterParameters(search: string): void {
|
||||
const filteredRowIndexes: number[] = [];
|
||||
this.parametersTable.data?.forEach((row, index) => {
|
||||
if (row[0].toUpperCase()?.search(search.toUpperCase()) !== -1 || row[2].toUpperCase()?.search(search.toUpperCase()) !== -1) {
|
||||
filteredRowIndexes.push(index);
|
||||
}
|
||||
});
|
||||
this.parametersTable.setFilter(filteredRowIndexes);
|
||||
}
|
||||
|
||||
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;
|
||||
return false;
|
||||
} else if (component.value === currentValue) {
|
||||
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;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private 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!),
|
||||
validationErrorMessage: loc.outOfRange(engineSetting.min!, 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.allowedValue(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,
|
||||
title: loc.resetToDefault,
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
enabled: true
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
resetParameterButton.onDidClick(async () => {
|
||||
try {
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: loc.updatingInstance(this._postgresModel.info.name),
|
||||
cancellable: false
|
||||
},
|
||||
async (_progress, _token): Promise<void> => {
|
||||
const session = await this._postgresModel.controllerModel.acquireAzdataSession();
|
||||
try {
|
||||
await this._azdataApi.azdata.arc.postgres.server.edit(
|
||||
this._postgresModel.info.name,
|
||||
{ engineSettings: engineSetting.parameterName + '=' },
|
||||
this._postgresModel.engineVersion,
|
||||
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
|
||||
session);
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
await this._postgresModel.refresh();
|
||||
}
|
||||
);
|
||||
|
||||
vscode.window.showInformationMessage(loc.instanceUpdated(this._postgresModel.info.name));
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.instanceUpdateFailed(this._postgresModel.info.name, error));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let parameter: ParametersModel = {
|
||||
parameterName: engineSetting.parameterName!,
|
||||
valueContainer: valueContainer,
|
||||
description: engineSetting.description!,
|
||||
resetButton: resetParameterButton
|
||||
};
|
||||
|
||||
return parameter;
|
||||
}
|
||||
|
||||
private refreshParametersTable(): void {
|
||||
this._parameters = this._postgresModel._engineSettings.map(engineSetting => this.createParameterComponents(engineSetting));
|
||||
this.parametersTable.data = this._parameters.map(p => [p.parameterName, p.valueContainer, p.description, p.resetButton]);
|
||||
}
|
||||
|
||||
private 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,8 @@ export class PostgresPropertiesPage extends DashboardPage {
|
||||
private loading?: azdata.LoadingComponent;
|
||||
private keyValueContainer?: KeyValueContainer;
|
||||
|
||||
constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
|
||||
super(modelView);
|
||||
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
|
||||
super(modelView, dashboard);
|
||||
|
||||
this.disposables.push(this._postgresModel.onConfigUpdated(
|
||||
() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())));
|
||||
@@ -100,13 +100,12 @@ export class PostgresPropertiesPage extends DashboardPage {
|
||||
return [
|
||||
new InputKeyValue(this.modelView.modelBuilder, loc.coordinatorEndpoint, endpoint ? `postgresql://postgres@${endpoint.ip}:${endpoint.port}` : ''),
|
||||
new InputKeyValue(this.modelView.modelBuilder, loc.postgresAdminUsername, 'postgres'),
|
||||
new TextKeyValue(this.modelView.modelBuilder, loc.status, status ? `${status.state} (${status.readyPods} ${loc.podsReady})` : loc.unknown),
|
||||
new LinkKeyValue(this.modelView.modelBuilder, loc.dataController, this._controllerModel.controllerConfig?.metadata.namespace ?? '', () => controllerDashboard.showDashboard()),
|
||||
new TextKeyValue(this.modelView.modelBuilder, loc.dataController, this._controllerModel.controllerConfig?.metadata.namespace ?? ''),
|
||||
new TextKeyValue(this.modelView.modelBuilder, loc.nodeConfiguration, this._postgresModel.scaleConfiguration ?? ''),
|
||||
new TextKeyValue(this.modelView.modelBuilder, loc.postgresVersion, this._postgresModel.engineVersion ?? ''),
|
||||
new InputKeyValue(this.modelView.modelBuilder, loc.subscriptionId, this._controllerModel.controllerConfig?.spec.settings.azure.subscription ?? ''),
|
||||
new TextKeyValue(this.modelView.modelBuilder, loc.resourceGroup, this._controllerModel.controllerConfig?.spec.settings.azure.resourceGroup ?? ''),
|
||||
new TextKeyValue(this.modelView.modelBuilder, loc.subscriptionId, this._controllerModel.controllerConfig?.spec.settings.azure.subscription ?? '')
|
||||
new LinkKeyValue(this.modelView.modelBuilder, loc.dataController, this._controllerModel.controllerConfig?.metadata.name ?? '', () => controllerDashboard.showDashboard()),
|
||||
new TextKeyValue(this.modelView.modelBuilder, loc.nodeConfiguration, this._postgresModel.scaleConfiguration ?? ''),
|
||||
new TextKeyValue(this.modelView.modelBuilder, loc.status, status ? `${status.state} (${status.readyPods} ${loc.podsReady})` : loc.unknown),
|
||||
new TextKeyValue(this.modelView.modelBuilder, loc.postgresVersion, this._postgresModel.engineVersion ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,8 @@ import { ResourceType } from 'arc';
|
||||
import { PostgresModel } from '../../../models/postgresModel';
|
||||
|
||||
export class PostgresSupportRequestPage extends DashboardPage {
|
||||
constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
|
||||
super(modelView);
|
||||
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
|
||||
super(modelView, dashboard);
|
||||
}
|
||||
|
||||
protected get title(): string {
|
||||
@@ -44,6 +44,11 @@ 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,
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { IconPathHelper } from '../../../constants';
|
||||
import { PostgresParametersPage } from './postgresParameters';
|
||||
import { PostgresModel, EngineSettingsModel } from '../../../models/postgresModel';
|
||||
|
||||
export class PostgresWorkerNodeParametersPage extends PostgresParametersPage {
|
||||
|
||||
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, postgresModel: PostgresModel) {
|
||||
super(modelView, dashboard, postgresModel);
|
||||
}
|
||||
|
||||
protected get title(): string {
|
||||
// TODO update to loc.workerNodeParameters
|
||||
return loc.nodeParameters;
|
||||
}
|
||||
|
||||
protected get id(): string {
|
||||
// TODO update to 'postgres-worker-node-parameters'
|
||||
return 'postgres-nodes-parameters';
|
||||
}
|
||||
|
||||
protected get icon(): { dark: string; light: string; } {
|
||||
return IconPathHelper.gearBlue;
|
||||
}
|
||||
|
||||
protected get description(): string {
|
||||
// TODO update to loc.workerNodesParametersDescription
|
||||
return loc.nodeParametersDescription;
|
||||
}
|
||||
|
||||
|
||||
protected get engineSettings(): EngineSettingsModel[] {
|
||||
return this._postgresModel.workerNodesEngineSettings;
|
||||
}
|
||||
|
||||
protected async saveParameterEdits(engineSettings: string): Promise<void> {
|
||||
await this._azdataApi.azdata.arc.postgres.server.edit(
|
||||
this._postgresModel.info.name,
|
||||
{ engineSettings: engineSettings },
|
||||
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
|
||||
this._postgresModel.controllerModel.controllerContext);
|
||||
}
|
||||
|
||||
protected async resetAllParameters(): Promise<void> {
|
||||
await this._azdataApi.azdata.arc.postgres.server.edit(
|
||||
this._postgresModel.info.name,
|
||||
{ engineSettings: `''`, replaceEngineSettings: true },
|
||||
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
|
||||
this._postgresModel.controllerModel.controllerContext);
|
||||
}
|
||||
|
||||
protected async resetParameter(parameterName: string): Promise<void> {
|
||||
await this._azdataApi.azdata.arc.postgres.server.edit(
|
||||
this._postgresModel.info.name,
|
||||
{ engineSettings: parameterName + '=' },
|
||||
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
|
||||
this._postgresModel.controllerModel.controllerContext);
|
||||
}
|
||||
}
|
||||
@@ -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 } from '../../common/kubeUtils';
|
||||
import { getCurrentClusterContext, getDefaultKubeConfigPath, getKubeConfigClusterContexts, KubeClusterContext } from '../../common/kubeUtils';
|
||||
import { FilePicker } from '../components/filePicker';
|
||||
|
||||
export type ConnectToControllerDialogModel = { controllerModel: ControllerModel, password: string };
|
||||
@@ -25,24 +25,34 @@ abstract class ControllerDialogBase extends InitializingComponent {
|
||||
protected modelBuilder!: azdata.ModelBuilder;
|
||||
protected dialog: azdata.window.Dialog;
|
||||
|
||||
protected urlInputBox!: azdata.InputBoxComponent;
|
||||
protected namespaceInputBox!: 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; // clear the _toDispose array
|
||||
this._toDispose.length = 0;
|
||||
}
|
||||
|
||||
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,
|
||||
required: true
|
||||
layout: {
|
||||
info: loc.controllerUrlDescription
|
||||
}
|
||||
}, {
|
||||
component: this.kubeConfigInputBox.component(),
|
||||
title: loc.controllerKubeConfig,
|
||||
@@ -54,14 +64,17 @@ abstract class ControllerDialogBase extends InitializingComponent {
|
||||
}, {
|
||||
component: this.nameInputBox,
|
||||
title: loc.controllerName,
|
||||
required: false
|
||||
required: false,
|
||||
layout: {
|
||||
info: loc.controllerNameDescription
|
||||
}
|
||||
}, {
|
||||
component: this.usernameInputBox,
|
||||
title: loc.username,
|
||||
title: loc.controllerUsername,
|
||||
required: true
|
||||
}, {
|
||||
component: this.passwordInputBox,
|
||||
title: loc.password,
|
||||
title: loc.controllerPassword,
|
||||
required: true
|
||||
}
|
||||
];
|
||||
@@ -71,11 +84,14 @@ 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()
|
||||
.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
|
||||
.withProps({
|
||||
value: controllerInfo?.endpoint,
|
||||
placeHolder: loc.controllerUrlPlaceholder,
|
||||
}).component();
|
||||
this.kubeConfigInputBox = new FilePicker(
|
||||
this.modelBuilder,
|
||||
@@ -83,22 +99,23 @@ abstract class ControllerDialogBase extends InitializingComponent {
|
||||
(disposable) => this._toDispose.push(disposable)
|
||||
);
|
||||
this.modelBuilder.inputBox()
|
||||
.withProperties<azdata.InputBoxProperties>({
|
||||
.withProps({
|
||||
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()
|
||||
.withProperties<azdata.InputBoxProperties>({
|
||||
.withProps({
|
||||
value: controllerInfo?.name
|
||||
}).component();
|
||||
this.usernameInputBox = this.modelBuilder.inputBox()
|
||||
.withProperties<azdata.InputBoxProperties>({
|
||||
.withProps({
|
||||
value: controllerInfo?.username
|
||||
}).component();
|
||||
this.passwordInputBox = this.modelBuilder.inputBox()
|
||||
.withProperties<azdata.InputBoxProperties>({
|
||||
.withProps({
|
||||
inputType: 'password',
|
||||
value: password
|
||||
}).component();
|
||||
@@ -114,15 +131,22 @@ abstract class ControllerDialogBase extends InitializingComponent {
|
||||
}
|
||||
|
||||
private loadRadioGroup(previousClusterContext?: string): void {
|
||||
this.clusterContextRadioGroup.load(async () => {
|
||||
const clusters = await getKubeConfigClusterContexts(this.kubeConfigInputBox.value!);
|
||||
this.clusterContextRadioGroup.load(() => {
|
||||
this._kubeClusters = getKubeConfigClusterContexts(this.kubeConfigInputBox.value!);
|
||||
const currentClusterContext = getCurrentClusterContext(this._kubeClusters, previousClusterContext, false);
|
||||
this.namespaceInputBox.value = currentClusterContext.namespace || this.namespaceInputBox.value;
|
||||
return {
|
||||
values: clusters.map(c => c.name),
|
||||
defaultValue: getCurrentClusterContext(clusters, previousClusterContext, false),
|
||||
values: this._kubeClusters.map(c => c.name),
|
||||
defaultValue: currentClusterContext.name
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
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 ?? [];
|
||||
@@ -168,7 +192,8 @@ abstract class ControllerDialogBase extends InitializingComponent {
|
||||
protected getControllerInfo(url: string, rememberPassword: boolean = false): ControllerInfo {
|
||||
return {
|
||||
id: this.id,
|
||||
url: url,
|
||||
endpoint: url || undefined,
|
||||
namespace: this.namespaceInputBox.value!,
|
||||
kubeConfigFilePath: this.kubeConfigInputBox.value!,
|
||||
kubeClusterContext: this.clusterContextRadioGroup.value!,
|
||||
name: this.nameInputBox.value ?? '',
|
||||
@@ -183,7 +208,7 @@ export class ConnectToControllerDialog extends ControllerDialogBase {
|
||||
protected rememberPwCheckBox!: azdata.CheckBoxComponent;
|
||||
|
||||
protected fieldToFocusOn() {
|
||||
return this.urlInputBox;
|
||||
return this.namespaceInputBox;
|
||||
}
|
||||
|
||||
protected getComponents() {
|
||||
@@ -209,22 +234,25 @@ export class ConnectToControllerDialog extends ControllerDialogBase {
|
||||
}
|
||||
|
||||
public async validate(): Promise<boolean> {
|
||||
if (!this.urlInputBox.value || !this.usernameInputBox.value || !this.passwordInputBox.value) {
|
||||
if (!this.namespaceInputBox.value || !this.usernameInputBox.value || !this.passwordInputBox.value) {
|
||||
return false;
|
||||
}
|
||||
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`;
|
||||
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`;
|
||||
}
|
||||
}
|
||||
|
||||
const controllerInfo: ControllerInfo = this.getControllerInfo(url, !!this.rememberPwCheckBox.checked);
|
||||
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
|
||||
try {
|
||||
@@ -234,7 +262,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.urlInputBox.value, err),
|
||||
text: loc.connectToControllerFailed(this.namespaceInputBox.value, err),
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
return false;
|
||||
@@ -267,11 +295,16 @@ 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(
|
||||
this.urlInputBox.value!,
|
||||
this.usernameInputBox.value!,
|
||||
{
|
||||
endpoint: controllerInfo.endpoint,
|
||||
namespace: controllerInfo.namespace
|
||||
},
|
||||
controllerInfo.username,
|
||||
this.passwordInputBox.value,
|
||||
{
|
||||
'KUBECONFIG': this.kubeConfigInputBox.value!,
|
||||
@@ -293,8 +326,6 @@ 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;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ControllerModel } from '../../models/controllerModel';
|
||||
import { ControllerTreeNode } from './controllerTreeNode';
|
||||
import { TreeNode } from './treeNode';
|
||||
|
||||
const mementoToken = 'arcDataControllers';
|
||||
const mementoToken = 'arcDataControllers.v2';
|
||||
|
||||
/**
|
||||
* The TreeDataProvider for the Azure Arc view, which displays a list of registered
|
||||
|
||||
@@ -44,7 +44,7 @@ export class ControllerTreeNode extends TreeNode {
|
||||
} catch (err) {
|
||||
vscode.window.showErrorMessage(loc.errorConnectingToController(err));
|
||||
try {
|
||||
await this.model.refresh(false, true);
|
||||
await this.model.refresh(false);
|
||||
this.updateChildren(this.model.registrations);
|
||||
} catch (err) {
|
||||
if (!(err instanceof UserCancelledError)) {
|
||||
|
||||
@@ -182,10 +182,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd"
|
||||
integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==
|
||||
|
||||
"@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==
|
||||
"@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==
|
||||
dependencies:
|
||||
http-proxy-agent "^2.1.0"
|
||||
https-proxy-agent "^2.2.4"
|
||||
|
||||
@@ -0,0 +1,486 @@
|
||||
{
|
||||
"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": [
|
||||
"\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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"name": "python3",
|
||||
"display_name": "Python 3"
|
||||
"display_name": "Python 3",
|
||||
"language": "python"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
@@ -162,21 +163,15 @@
|
||||
"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'"
|
||||
"sql_collation = 'SQL_Latin1_General_CP1_CI_AS'\n",
|
||||
"vm_size = 'Standard_DS1_v2'"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "19ebeaf4-94c9-4d2b-bd9f-e3c6bf7f2dda",
|
||||
@@ -265,6 +260,34 @@
|
||||
"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": [
|
||||
@@ -319,25 +342,26 @@
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### Create network security group"
|
||||
"### Add the Edge device to the IoT hub"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "b308771b-138a-40ce-a9d3-1d15094d537b"
|
||||
"azdata_cell_guid": "fbc5f4ac-dfe0-4543-ace1-49b796251910"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"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",
|
||||
"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'Network security group \\\"{network_security_group}\\\" already exists.')"
|
||||
" 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": "99cbb95c-b109-4b2e-909b-ff71a62754fb",
|
||||
"azdata_cell_guid": "c183c3e3-8699-4f29-993b-07bf848336e3",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
@@ -357,103 +381,25 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"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)"
|
||||
"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')"
|
||||
],
|
||||
"metadata": {
|
||||
"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",
|
||||
"azdata_cell_guid": "c8590c65-b274-460d-9659-97e81d2fd3ea",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "asde-deployment",
|
||||
"displayName": "%extension-displayName%",
|
||||
"description": "%extension-description%",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.2",
|
||||
"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": "*"
|
||||
"azdata": ">=1.25.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -43,9 +43,13 @@
|
||||
"displayName": "%sql-edge-remote-display-name%"
|
||||
},
|
||||
{
|
||||
"name": "azure-create-new",
|
||||
"name": "azure-create-new-password-auth",
|
||||
"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%"
|
||||
@@ -59,6 +63,7 @@
|
||||
],
|
||||
"providers": [
|
||||
{
|
||||
"name": "sql-edge_local",
|
||||
"dialog": {
|
||||
"notebook": "./notebooks/edge/deploy-sql-edge-local.ipynb",
|
||||
"title": "%sql-edge-local-title%",
|
||||
@@ -152,6 +157,7 @@
|
||||
"when": "type=local"
|
||||
},
|
||||
{
|
||||
"name": "sql-edge_remote",
|
||||
"dialog": {
|
||||
"notebook": "./notebooks/edge/deploy-sql-edge-remote.ipynb",
|
||||
"title": "%sql-edge-remote-title%",
|
||||
@@ -269,6 +275,7 @@
|
||||
"when": "type=remote"
|
||||
},
|
||||
{
|
||||
"name": "sql-edge-azure-create-new-password-auth",
|
||||
"dialog": {
|
||||
"notebook": "./notebooks/edge/deploy-sql-edge-azure.ipynb",
|
||||
"title": "%sql-edge-azure-title%",
|
||||
@@ -396,9 +403,132 @@
|
||||
"version": "2.13.0"
|
||||
}
|
||||
],
|
||||
"when": "type=azure-create-new"
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"name": "sql-edge_azure-single-device",
|
||||
"dialog": {
|
||||
"notebook": "./notebooks/edge/deploy-sql-edge-single-device.ipynb",
|
||||
"title": "%sql-edge-azure-single-device-title%",
|
||||
@@ -489,6 +619,7 @@
|
||||
"when": "type=azure-single-device"
|
||||
},
|
||||
{
|
||||
"name": "sql-edge_azure-multi-device",
|
||||
"dialog": {
|
||||
"notebook": "./notebooks/edge/deploy-sql-edge-multi-device.ipynb",
|
||||
"title": "%sql-edge-azure-multi-device-title%",
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
"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",
|
||||
"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-title": "Deploy Azure SQL Edge to a new Azure VM via IoT hub",
|
||||
"azure_subscription_id": "Subscription id",
|
||||
"azure_resource_group": "Resource group",
|
||||
@@ -48,5 +49,6 @@
|
||||
"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"
|
||||
"device-target-condition-learn-more": "Learn more about target condition",
|
||||
"vm_ssh_public_key": "SSH public key"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "azdata",
|
||||
"displayName": "%azdata.displayName%",
|
||||
"description": "%azdata.description%",
|
||||
"version": "0.5.1",
|
||||
"version": "0.6.2",
|
||||
"publisher": "Microsoft",
|
||||
"preview": true,
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
|
||||
@@ -107,7 +107,12 @@
|
||||
"when": "azdata.found"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"resourceDeploymentOptionsSources": [
|
||||
{
|
||||
"id": "arc.controller.config.profiles"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"request": "^2.88.2",
|
||||
|
||||
@@ -5,13 +5,26 @@
|
||||
|
||||
import * as azdataExt from 'azdata-ext';
|
||||
import * as vscode from 'vscode';
|
||||
import { IAzdataTool, isEulaAccepted, promptForEula } from './azdata';
|
||||
import { IAzdataTool, isEulaAccepted, MIN_AZDATA_VERSION, 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) {
|
||||
@@ -20,6 +33,13 @@ 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);
|
||||
@@ -55,47 +75,47 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
|
||||
profileName?: string,
|
||||
storageClass?: string,
|
||||
additionalEnvVars?: azdataExt.AdditionalEnvVars,
|
||||
session?: azdataExt.AzdataSession) => {
|
||||
azdataContext?: string) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.dc.create(namespace, name, connectivityMode, resourceGroup, location, subscription, profileName, storageClass, additionalEnvVars, session);
|
||||
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata!.arc.dc.create(namespace, name, connectivityMode, resourceGroup, location, subscription, profileName, storageClass, additionalEnvVars, azdataContext);
|
||||
},
|
||||
endpoint: {
|
||||
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
|
||||
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.dc.endpoint.list(additionalEnvVars, session);
|
||||
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata!.arc.dc.endpoint.list(additionalEnvVars, azdataContext);
|
||||
}
|
||||
},
|
||||
config: {
|
||||
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
|
||||
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.dc.config.list(additionalEnvVars, session);
|
||||
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata!.arc.dc.config.list(additionalEnvVars, azdataContext);
|
||||
},
|
||||
show: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
|
||||
show: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.dc.config.show(additionalEnvVars, session);
|
||||
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata!.arc.dc.config.show(additionalEnvVars, azdataContext);
|
||||
}
|
||||
}
|
||||
},
|
||||
postgres: {
|
||||
server: {
|
||||
delete: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
|
||||
delete: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.postgres.server.delete(name, additionalEnvVars, session);
|
||||
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata!.arc.postgres.server.delete(name, additionalEnvVars, azdataContext);
|
||||
},
|
||||
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
|
||||
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.postgres.server.list(additionalEnvVars, session);
|
||||
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata!.arc.postgres.server.list(additionalEnvVars, azdataContext);
|
||||
},
|
||||
show: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
|
||||
show: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.postgres.server.show(name, additionalEnvVars, session);
|
||||
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata!.arc.postgres.server.show(name, additionalEnvVars, azdataContext);
|
||||
},
|
||||
edit: async (
|
||||
name: string,
|
||||
@@ -112,31 +132,30 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
|
||||
replaceEngineSettings?: boolean;
|
||||
workers?: number;
|
||||
},
|
||||
engineVersion?: string,
|
||||
additionalEnvVars?: azdataExt.AdditionalEnvVars,
|
||||
session?: azdataExt.AzdataSession) => {
|
||||
azdataContext?: string) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.postgres.server.edit(name, args, engineVersion, additionalEnvVars, session);
|
||||
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata!.arc.postgres.server.edit(name, args, additionalEnvVars, azdataContext);
|
||||
}
|
||||
}
|
||||
},
|
||||
sql: {
|
||||
mi: {
|
||||
delete: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
|
||||
delete: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.sql.mi.delete(name, additionalEnvVars, session);
|
||||
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata!.arc.sql.mi.delete(name, additionalEnvVars, azdataContext);
|
||||
},
|
||||
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
|
||||
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.sql.mi.list(additionalEnvVars, session);
|
||||
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata!.arc.sql.mi.list(additionalEnvVars, azdataContext);
|
||||
},
|
||||
show: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
|
||||
show: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.sql.mi.show(name, additionalEnvVars, session);
|
||||
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata!.arc.sql.mi.show(name, additionalEnvVars, azdataContext);
|
||||
},
|
||||
edit: async (
|
||||
name: string,
|
||||
@@ -148,11 +167,11 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
|
||||
noWait?: boolean;
|
||||
},
|
||||
additionalEnvVars?: azdataExt.AdditionalEnvVars,
|
||||
session?: azdataExt.AzdataSession
|
||||
azdataContext?: string
|
||||
) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.sql.mi.edit(name, args, additionalEnvVars, session);
|
||||
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata!.arc.sql.mi.edit(name, args, additionalEnvVars, azdataContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,13 +181,9 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
|
||||
throwIfNoAzdata(azdataToolService.localAzdata);
|
||||
return azdataToolService.localAzdata.getPath();
|
||||
},
|
||||
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);
|
||||
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);
|
||||
},
|
||||
getSemVersion: async () => {
|
||||
await localAzdataDiscovered;
|
||||
|
||||
@@ -13,11 +13,15 @@ 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'
|
||||
@@ -32,20 +36,7 @@ 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): 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();
|
||||
}
|
||||
executeCommand<R>(args: string[], additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<R>>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,9 +45,6 @@ class AzdataSession implements azdataExt.AzdataSession {
|
||||
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);
|
||||
@@ -90,7 +78,7 @@ export class AzdataTool implements azdataExt.IAzdataApi {
|
||||
profileName?: string,
|
||||
storageClass?: string,
|
||||
additionalEnvVars?: azdataExt.AdditionalEnvVars,
|
||||
session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<void>> => {
|
||||
azdataContext?: string): Promise<azdataExt.AzdataOutput<void>> => {
|
||||
const args = ['arc', 'dc', 'create',
|
||||
'--namespace', namespace,
|
||||
'--name', name,
|
||||
@@ -104,32 +92,32 @@ export class AzdataTool implements azdataExt.IAzdataApi {
|
||||
if (storageClass) {
|
||||
args.push('--storage-class', storageClass);
|
||||
}
|
||||
return this.executeCommand<void>(args, additionalEnvVars, session);
|
||||
return this.executeCommand<void>(args, additionalEnvVars, azdataContext);
|
||||
},
|
||||
endpoint: {
|
||||
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.DcEndpointListResult[]>> => {
|
||||
return this.executeCommand<azdataExt.DcEndpointListResult[]>(['arc', 'dc', 'endpoint', 'list'], additionalEnvVars, session);
|
||||
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.DcEndpointListResult[]>> => {
|
||||
return this.executeCommand<azdataExt.DcEndpointListResult[]>(['arc', 'dc', 'endpoint', 'list'], additionalEnvVars, azdataContext);
|
||||
}
|
||||
},
|
||||
config: {
|
||||
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigListResult[]>> => {
|
||||
return this.executeCommand<azdataExt.DcConfigListResult[]>(['arc', 'dc', 'config', 'list'], additionalEnvVars, session);
|
||||
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigListResult[]>> => {
|
||||
return this.executeCommand<azdataExt.DcConfigListResult[]>(['arc', 'dc', 'config', 'list'], 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);
|
||||
show: (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigShowResult>> => {
|
||||
return this.executeCommand<azdataExt.DcConfigShowResult>(['arc', 'dc', 'config', 'show'], additionalEnvVars, azdataContext);
|
||||
}
|
||||
}
|
||||
},
|
||||
postgres: {
|
||||
server: {
|
||||
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);
|
||||
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);
|
||||
},
|
||||
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerListResult[]>> => {
|
||||
return this.executeCommand<azdataExt.PostgresServerListResult[]>(['arc', 'postgres', 'server', 'list'], additionalEnvVars, session);
|
||||
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerListResult[]>> => {
|
||||
return this.executeCommand<azdataExt.PostgresServerListResult[]>(['arc', 'postgres', 'server', 'list'], 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);
|
||||
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);
|
||||
},
|
||||
edit: (
|
||||
name: string,
|
||||
@@ -146,9 +134,8 @@ export class AzdataTool implements azdataExt.IAzdataApi {
|
||||
replaceEngineSettings?: boolean,
|
||||
workers?: number
|
||||
},
|
||||
engineVersion?: string,
|
||||
additionalEnvVars?: azdataExt.AdditionalEnvVars,
|
||||
session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<void>> => {
|
||||
azdataContext?: string): 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); }
|
||||
@@ -161,21 +148,20 @@ 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()); }
|
||||
if (engineVersion) { argsArray.push('--engine-version', engineVersion); }
|
||||
return this.executeCommand<void>(argsArray, additionalEnvVars, session);
|
||||
return this.executeCommand<void>(argsArray, additionalEnvVars, azdataContext);
|
||||
}
|
||||
}
|
||||
},
|
||||
sql: {
|
||||
mi: {
|
||||
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);
|
||||
delete: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<void>> => {
|
||||
return this.executeCommand<void>(['arc', 'sql', 'mi', 'delete', '-n', name], 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);
|
||||
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiListResult[]>> => {
|
||||
return this.executeCommand<azdataExt.SqlMiListResult[]>(['arc', 'sql', 'mi', 'list'], 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);
|
||||
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);
|
||||
},
|
||||
edit: (
|
||||
name: string,
|
||||
@@ -186,8 +172,7 @@ export class AzdataTool implements azdataExt.IAzdataApi {
|
||||
memoryRequest?: string,
|
||||
noWait?: boolean,
|
||||
},
|
||||
additionalEnvVars?: azdataExt.AdditionalEnvVars,
|
||||
session?: azdataExt.AzdataSession
|
||||
additionalEnvVars?: azdataExt.AdditionalEnvVars
|
||||
): Promise<azdataExt.AzdataOutput<void>> => {
|
||||
const argsArray = ['arc', 'sql', 'mi', 'edit', '-n', name];
|
||||
if (args.coresLimit) { argsArray.push('--cores-limit', args.coresLimit); }
|
||||
@@ -195,59 +180,22 @@ 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, session);
|
||||
return this.executeCommand<void>(argsArray, additionalEnvVars);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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];
|
||||
}
|
||||
// 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;
|
||||
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 {
|
||||
// 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;
|
||||
throw new Error(loc.endpointOrNamespaceRequired);
|
||||
}
|
||||
|
||||
await this.login(endpoint, username, password, additionalEnvVars);
|
||||
return session;
|
||||
return this.executeCommand<void>(args, Object.assign({}, additionalEnvVars, { 'AZDATA_PASSWORD': password }), azdataContext);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -265,34 +213,16 @@ 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. This is NOT session-aware so should only be used for calls that don't care about a session
|
||||
* Executes the specified azdata command.
|
||||
* @param args The args to pass to azdata
|
||||
* @param additionalEnvVars Additional environment variables to set for this execution
|
||||
*/
|
||||
private async executeCommandImpl<R>(args: string[], additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataOutput<R>> {
|
||||
public async executeCommand<R>(args: string[], additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): 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,
|
||||
@@ -442,8 +372,22 @@ 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();
|
||||
if (newSemVersion.compare(await currentAzdata.getSemVersion()) === 1) {
|
||||
Logger.log(loc.foundAzdataVersionToUpdateTo(newSemVersion.raw, (await currentAzdata.getSemVersion()).raw));
|
||||
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) {
|
||||
return await promptToUpdateAzdata(newSemVersion.raw, userRequested);
|
||||
} else {
|
||||
Logger.log(loc.currentlyInstalledVersionIsLatest((await currentAzdata.getSemVersion()).raw));
|
||||
@@ -504,39 +448,65 @@ 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): 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);
|
||||
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);
|
||||
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.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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
/**
|
||||
* Deferred promise
|
||||
*/
|
||||
export class Deferred<T> {
|
||||
export class Deferred<T = void> {
|
||||
promise: Promise<T>;
|
||||
resolve!: (value?: T | PromiseLike<T>) => void;
|
||||
resolve!: (value: T | PromiseLike<T>) => void;
|
||||
reject!: (reason?: any) => void;
|
||||
constructor() {
|
||||
this.promise = new Promise<T>((resolve, reject) => {
|
||||
this.resolve = <any>resolve;
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -38,8 +38,11 @@ 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);
|
||||
@@ -66,3 +69,4 @@ 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");
|
||||
|
||||
@@ -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('', '1.0.0');
|
||||
const azdataTool = new AzdataTool('', '99.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: `1.0.0`, stderr: '' };
|
||||
return { stdout: `99.0.0`, stderr: '' };
|
||||
}
|
||||
console.log(args[0]);
|
||||
return { stdout: `{ }`, stderr: '' };
|
||||
@@ -96,15 +96,8 @@ 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('', '', ''), 'login');
|
||||
await assertCallback((async () => {
|
||||
let session: azdataExt.AzdataSession | undefined;
|
||||
try {
|
||||
session = await api.azdata.acquireSession('', '', '');
|
||||
} finally {
|
||||
session?.dispose();
|
||||
}
|
||||
})(), 'acquireSession');
|
||||
await assertCallback(api.azdata.login({ endpoint: 'https://127.0.0.1' }, '', ''), 'login');
|
||||
await assertCallback(api.azdata.login({ namespace: 'namespace' }, '', ''), 'login');
|
||||
await assertCallback(api.azdata.version(), 'version');
|
||||
|
||||
await assertCallback(api.azdata.arc.dc.create('', '', '', '', '', ''), 'arc dc create');
|
||||
@@ -117,7 +110,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');
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* 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';
|
||||
@@ -17,9 +16,8 @@ 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', '0.0.0');
|
||||
const oldAzdataMock = new azdata.AzdataTool('/path/to/azdata', azdata.MIN_AZDATA_VERSION.raw);
|
||||
const currentAzdataMock = new azdata.AzdataTool('/path/to/azdata', '9999.999.999');
|
||||
|
||||
/**
|
||||
@@ -222,120 +220,10 @@ describe('azdata', function () {
|
||||
const endpoint = 'myEndpoint';
|
||||
const username = 'myUsername';
|
||||
const password = 'myPassword';
|
||||
await azdataTool.login(endpoint, username, password);
|
||||
await azdataTool.login({ endpoint: 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();
|
||||
@@ -473,6 +361,7 @@ describe('azdata', function () {
|
||||
beforeEach(function (): void {
|
||||
showInformationMessageStub = sinon.stub(vscode.window, 'showInformationMessage').returns(Promise.resolve(<any>loc.yes));
|
||||
executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand').returns(Promise.resolve({ stdout: '', stderr: '' }));
|
||||
sinon.stub(HttpClient, 'getTextContent').resolves(JSON.stringify(releaseJson));
|
||||
});
|
||||
|
||||
it('successful update', async function (): Promise<void> {
|
||||
@@ -577,7 +466,6 @@ describe('azdata', function () {
|
||||
|
||||
describe('discoverLatestAvailableAzdataVersion', function (): void {
|
||||
it('finds latest available version of azdata successfully', async function (): Promise<void> {
|
||||
sinon.stub(HttpClient, 'getTextContent').resolves(JSON.stringify(releaseJson));
|
||||
await azdata.discoverLatestAvailableAzdataVersion();
|
||||
});
|
||||
});
|
||||
@@ -706,7 +594,6 @@ async function testWin32UnsuccessfulUpdate() {
|
||||
}
|
||||
|
||||
async function testLinuxSuccessfulUpdate(userRequested = false) {
|
||||
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
|
||||
const executeCommandStub = sinon.stub(childProcess, 'executeCommand').returns(Promise.resolve({ stdout: '0.0.0', stderr: '' }));
|
||||
executeSudoCommandStub.resolves({ stdout: '0.0.0', stderr: '' });
|
||||
await azdata.checkAndUpdateAzdata(oldAzdataMock, userRequested);
|
||||
@@ -740,7 +627,6 @@ async function testDarwinSuccessfulUpdate(userRequested = false) {
|
||||
|
||||
|
||||
async function testWin32SuccessfulUpdate(userRequested = false) {
|
||||
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
|
||||
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
|
||||
await azdata.checkAndUpdateAzdata(oldAzdataMock, userRequested);
|
||||
should(executeSudoCommandStub.calledOnce).be.true('executeSudoCommand should have been called once');
|
||||
@@ -748,7 +634,6 @@ async function testWin32SuccessfulUpdate(userRequested = false) {
|
||||
}
|
||||
|
||||
async function testLinuxSkippedUpdate() {
|
||||
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
|
||||
executeSudoCommandStub.resolves({ stdout: '0.0.0', stderr: '' });
|
||||
await azdata.checkAndUpdateAzdata(currentAzdataMock);
|
||||
should(executeSudoCommandStub.callCount).be.equal(0, 'executeSudoCommand was not expected to be called');
|
||||
@@ -778,14 +663,12 @@ async function testDarwinSkippedUpdateDontPrompt() {
|
||||
}
|
||||
|
||||
async function testWin32SkippedUpdateDontPrompt() {
|
||||
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
|
||||
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
|
||||
await azdata.checkAndUpdateAzdata(oldAzdataMock);
|
||||
should(executeSudoCommandStub.notCalled).be.true('executeSudoCommand should not have been called');
|
||||
should(executeSudoCommandStub.notCalled).be.true(`executeSudoCommand should not have been called ${executeSudoCommandStub.getCalls().join(os.EOL)}`);
|
||||
}
|
||||
|
||||
async function testLinuxSkippedUpdateDontPrompt() {
|
||||
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
|
||||
sinon.stub(childProcess, 'executeCommand').returns(Promise.resolve({ stdout: '0.0.0', stderr: '' }));
|
||||
executeSudoCommandStub.resolves({ stdout: '0.0.0', stderr: '' });
|
||||
await azdata.checkAndUpdateAzdata(oldAzdataMock);
|
||||
@@ -816,7 +699,6 @@ async function testDarwinSkippedUpdate() {
|
||||
}
|
||||
|
||||
async function testWin32SkippedUpdate() {
|
||||
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
|
||||
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
|
||||
await azdata.checkAndUpdateAzdata(currentAzdataMock);
|
||||
should(executeSudoCommandStub.notCalled).be.true('executeSudoCommand should not have been called');
|
||||
@@ -849,7 +731,6 @@ async function testLinuxSkippedInstall() {
|
||||
}
|
||||
|
||||
async function testWin32SkippedInstall() {
|
||||
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
|
||||
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
|
||||
sinon.stub(childProcess, 'executeCommand')
|
||||
.onFirstCall()
|
||||
@@ -863,7 +744,6 @@ async function testWin32SkippedInstall() {
|
||||
}
|
||||
|
||||
async function testWin32SuccessfulInstall() {
|
||||
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
|
||||
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
|
||||
const executeCommandStub = sinon.stub(childProcess, 'executeCommand')
|
||||
.onFirstCall()
|
||||
|
||||
@@ -18,7 +18,3 @@ 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));
|
||||
}
|
||||
|
||||
|
||||
60
extensions/azdata/src/typings/azdata-ext.d.ts
vendored
60
extensions/azdata/src/typings/azdata-ext.d.ts
vendored
@@ -17,7 +17,7 @@ declare module 'azdata-ext' {
|
||||
name = 'Microsoft.azdata'
|
||||
}
|
||||
|
||||
export type AdditionalEnvVars = { [key: string]: string};
|
||||
export type AdditionalEnvVars = { [key: string]: string };
|
||||
|
||||
export interface ErrorWithLink extends Error {
|
||||
messageWithLink: string;
|
||||
@@ -160,7 +160,7 @@ declare module 'azdata-ext' {
|
||||
|
||||
export interface PostgresServerShowResult {
|
||||
apiVersion: string, // "arcdata.microsoft.com/v1alpha1"
|
||||
kind: string, // "postgresql-12"
|
||||
kind: string, // "postgresql"
|
||||
metadata: {
|
||||
creationTimestamp: string, // "2020-08-19T20:25:11Z"
|
||||
generation: number, // 1
|
||||
@@ -177,7 +177,8 @@ 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)
|
||||
@@ -222,6 +223,17 @@ declare module 'azdata-ext' {
|
||||
state: string, // "Ready"
|
||||
logSearchDashboard: string, // https://127.0.0.1:30777/kibana/app/kibana#/discover?_a=(query:(language:kuery,query:'custom_resource_name:pg1'))
|
||||
metricsDashboard: string, // https://127.0.0.1:30777/grafana/d/40q72HnGk/sql-managed-instance-metrics?var-hostname=pg1
|
||||
podsStatus: {
|
||||
conditions: {
|
||||
lastTransitionTime: string, // "2020-08-19T17:05:39Z"
|
||||
message?: string, // "containers with unready status: [fluentbit postgres telegraf]"
|
||||
reason?: string, // "ContainersNotReady"
|
||||
status: string, // "True"
|
||||
type: string // "Ready"
|
||||
}[],
|
||||
name: string, // "pg-instancew-0",
|
||||
role: string // "worker"
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,25 +245,27 @@ declare module 'azdata-ext' {
|
||||
code?: number
|
||||
}
|
||||
|
||||
export interface AzdataSession extends vscode.Disposable { }
|
||||
|
||||
export interface EndpointOrNamespace {
|
||||
endpoint?: string,
|
||||
namespace?: string
|
||||
}
|
||||
export interface IAzdataApi {
|
||||
arc: {
|
||||
dc: {
|
||||
create(namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string, additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<void>>,
|
||||
create(namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<void>>,
|
||||
endpoint: {
|
||||
list(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<DcEndpointListResult[]>>
|
||||
list(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<DcEndpointListResult[]>>
|
||||
},
|
||||
config: {
|
||||
list(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<DcConfigListResult[]>>,
|
||||
show(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<DcConfigShowResult>>
|
||||
list(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<DcConfigListResult[]>>,
|
||||
show(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<DcConfigShowResult>>
|
||||
}
|
||||
},
|
||||
postgres: {
|
||||
server: {
|
||||
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>>,
|
||||
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>>,
|
||||
edit(
|
||||
name: string,
|
||||
args: {
|
||||
@@ -267,17 +281,16 @@ declare module 'azdata-ext' {
|
||||
replaceEngineSettings?: boolean,
|
||||
workers?: number
|
||||
},
|
||||
engineVersion?: string,
|
||||
additionalEnvVars?: AdditionalEnvVars,
|
||||
session?: AzdataSession
|
||||
azdataContext?: string
|
||||
): Promise<AzdataOutput<void>>
|
||||
}
|
||||
},
|
||||
sql: {
|
||||
mi: {
|
||||
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>>,
|
||||
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>>,
|
||||
edit(
|
||||
name: string,
|
||||
args: {
|
||||
@@ -288,22 +301,13 @@ declare module 'azdata-ext' {
|
||||
noWait?: boolean,
|
||||
},
|
||||
additionalEnvVars?: AdditionalEnvVars,
|
||||
session?: AzdataSession
|
||||
azdataContext?: string
|
||||
): Promise<AzdataOutput<void>>
|
||||
}
|
||||
}
|
||||
},
|
||||
getPath(): Promise<string>,
|
||||
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>,
|
||||
login(endpointOrNamespace: EndpointOrNamespace, username: string, password: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<void>>,
|
||||
/**
|
||||
* 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
|
||||
|
||||
@@ -317,6 +317,11 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"resourceDeploymentValueProviders": [
|
||||
{
|
||||
"id": "subscription-id-to-tenant-id"
|
||||
}
|
||||
],
|
||||
"hasAzureResourceProviders": true
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -12,10 +12,10 @@ import {
|
||||
AzureAccount,
|
||||
AzureAccountProviderMetadata,
|
||||
AzureAuthType,
|
||||
Deferred,
|
||||
Resource,
|
||||
Tenant
|
||||
} from '../interfaces';
|
||||
} from 'azurecore';
|
||||
import { Deferred } 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', "Specifed tenant with ID '{0}' not found.", tenantId), `Tenant ${tenantId} not found.`, undefined);
|
||||
throw new AzureAuthError(localize('azure.tenantNotFound', "Specified tenant with ID '{0}' not found.", tenantId), `Tenant ${tenantId} not found.`, undefined);
|
||||
}
|
||||
|
||||
const cachedTokens = await this.getSavedToken(tenant, resource, account.key);
|
||||
@@ -248,7 +248,7 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
|
||||
if (response.data.error) {
|
||||
Logger.error('Response error!', response.data);
|
||||
throw new AzureAuthError(localize('azure.responseError', "Token retrival failed with an error. Open developer tools to view the error"), 'Token retrival failed', undefined);
|
||||
throw new AzureAuthError(localize('azure.responseError', "Token retrieval failed with an error. Open developer tools to view the error"), 'Token retrieval failed', undefined);
|
||||
}
|
||||
|
||||
const accessTokenString = response.data.access_token;
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { AuthorizationCodePostData, AzureAuth, OAuthTokenResponse } from './azureAuth';
|
||||
import { AzureAccountProviderMetadata, AzureAuthType, Deferred, Resource, Tenant } from '../interfaces';
|
||||
import { AzureAccountProviderMetadata, AzureAuthType, Resource, Tenant } from 'azurecore';
|
||||
import { Deferred } from '../interfaces';
|
||||
import * as vscode from 'vscode';
|
||||
import * as crypto from 'crypto';
|
||||
import { SimpleTokenCache } from '../simpleTokenCache';
|
||||
|
||||
@@ -18,12 +18,9 @@ import {
|
||||
AzureAccountProviderMetadata,
|
||||
AzureAuthType,
|
||||
Tenant,
|
||||
Resource,
|
||||
Deferred,
|
||||
// Tenant,
|
||||
// Subscription
|
||||
} from '../interfaces';
|
||||
|
||||
Resource
|
||||
} from 'azurecore';
|
||||
import { Deferred } from '../interfaces';
|
||||
import { SimpleTokenCache } from '../simpleTokenCache';
|
||||
import { Logger } from '../../utils/Logger';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
@@ -10,9 +10,9 @@ import * as nls from 'vscode-nls';
|
||||
import {
|
||||
AzureAccountProviderMetadata,
|
||||
AzureAuthType,
|
||||
Deferred,
|
||||
AzureAccount
|
||||
} from './interfaces';
|
||||
} from 'azurecore';
|
||||
import { Deferred } 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, tenant: string, resource: azdata.AzureResource): Thenable<Token | undefined> {
|
||||
return this._getAccountSecurityToken(account, tenant, resource);
|
||||
getAccountSecurityToken(account: azdata.Account, tenantId: string, resource: azdata.AzureResource): Thenable<Token | undefined> {
|
||||
return this._getAccountSecurityToken(account, tenantId, resource);
|
||||
}
|
||||
|
||||
private async _getAccountSecurityToken(account: azdata.Account, tenant: string, resource: azdata.AzureResource): Promise<Token | undefined> {
|
||||
private async _getAccountSecurityToken(account: azdata.Account, tenantId: string, resource: azdata.AzureResource): Promise<Token | undefined> {
|
||||
await this.initCompletePromise;
|
||||
const azureAuth = this.getAuthMethod(undefined);
|
||||
return azureAuth?.getAccountSecurityToken(account, tenant, resource);
|
||||
return azureAuth?.getAccountSecurityToken(account, tenantId, resource);
|
||||
}
|
||||
|
||||
private async _getSecurityToken(account: azdata.Account, resource: azdata.AzureResource): Promise<MultiTenantTokenResponse | undefined> {
|
||||
|
||||
@@ -10,7 +10,8 @@ import * as vscode from 'vscode';
|
||||
import { SimpleTokenCache } from './simpleTokenCache';
|
||||
import providerSettings from './providerSettings';
|
||||
import { AzureAccountProvider as AzureAccountProvider } from './azureAccountProvider';
|
||||
import { AzureAccountProviderMetadata, ProviderSettings } from './interfaces';
|
||||
import { AzureAccountProviderMetadata } from 'azurecore';
|
||||
import { ProviderSettings } from './interfaces';
|
||||
import * as loc from '../localizedConstants';
|
||||
|
||||
let localize = nls.loadMessageBundle();
|
||||
|
||||
@@ -3,129 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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
|
||||
}
|
||||
import * as azurecore from 'azurecore';
|
||||
|
||||
/**
|
||||
* Mapping of configuration key with the metadata to instantiate the account provider
|
||||
@@ -139,44 +17,7 @@ export interface ProviderSettings {
|
||||
/**
|
||||
* Metadata for the provider
|
||||
*/
|
||||
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[];
|
||||
|
||||
metadata: azurecore.AzureAccountProviderMetadata;
|
||||
}
|
||||
|
||||
export interface Subscription {
|
||||
@@ -185,16 +26,6 @@ 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
|
||||
*/
|
||||
|
||||
@@ -76,7 +76,7 @@ export class AzureDataGridProvider implements azdata.DataGridProvider {
|
||||
{ id: 'icon', type: 'image', field: 'iconPath', name: '', width: 25, sortable: false, filterable: false, resizable: false, tooltip: loc.typeIcon },
|
||||
{ id: 'name', type: 'text', field: 'name', name: loc.name, width: 150 },
|
||||
{ id: 'type', type: 'text', field: 'typeDisplayName', name: loc.resourceType, width: 150 },
|
||||
{ id: 'type', type: 'text', field: 'resourceGroup', name: loc.resourceGroup, width: 150 },
|
||||
{ id: 'resourceGroup', type: 'text', field: 'resourceGroup', name: loc.resourceGroup, width: 150 },
|
||||
{ id: 'location', type: 'text', field: 'locationDisplayName', name: loc.location, width: 150 },
|
||||
{ id: 'subscriptionId', type: 'text', field: 'subscriptionName', name: loc.subscription, width: 150 }
|
||||
];
|
||||
|
||||
@@ -71,6 +71,39 @@ 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;
|
||||
|
||||
@@ -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 '../account-provider/interfaces';
|
||||
import { AzureAccount, Tenant } from 'azurecore';
|
||||
import { FlatAccountTreeNode } from './tree/flatAccountTreeNode';
|
||||
import { ConnectionDialogTreeProvider } from './tree/connectionDialogTreeProvider';
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import * as msRest from '@azure/ms-rest-js';
|
||||
import { Account } from 'azdata';
|
||||
|
||||
import { azureResource } from 'azureResource';
|
||||
import { AzureAccount, Tenant } from '../account-provider/interfaces';
|
||||
import { AzureAccount, Tenant } from 'azurecore';
|
||||
|
||||
export interface IAzureResourceSubscriptionService {
|
||||
getSubscriptions(account: Account, credential: msRest.ServiceClientCredentials, tenantId: string): Promise<azureResource.AzureResourceSubscription[]>;
|
||||
|
||||
@@ -9,7 +9,7 @@ import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import * as WS from 'ws';
|
||||
|
||||
import { IAzureTerminalService } from '../interfaces';
|
||||
import { AzureAccount, Tenant } from '../../account-provider/interfaces';
|
||||
import { AzureAccount, Tenant } from 'azurecore';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
|
||||
@@ -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 '../../account-provider/interfaces';
|
||||
import { AzureAccount } from 'azurecore';
|
||||
|
||||
export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNodeBase {
|
||||
public constructor(
|
||||
|
||||
@@ -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 '../../account-provider/interfaces';
|
||||
import { AzureAccount } from 'azurecore';
|
||||
import { AzureResourceService } from '../resourceService';
|
||||
import { AzureResourceResourceTreeNode } from '../resourceTreeNode';
|
||||
import { AzureResourceErrorMessageUtil } from '../utils';
|
||||
|
||||
@@ -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 } from 'azurecore';
|
||||
import { AzureRestResponse, GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult, GetBlobContainersResult, GetFileSharesResult, HttpRequestMethod, GetLocationsResult, GetManagedDatabasesResult } from 'azurecore';
|
||||
import { azureResource } from 'azureResource';
|
||||
import { EOL } from 'os';
|
||||
import * as nls from 'vscode-nls';
|
||||
@@ -142,6 +142,40 @@ 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[],
|
||||
@@ -395,6 +429,15 @@ 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);
|
||||
|
||||
175
extensions/azurecore/src/azurecore.d.ts
vendored
175
extensions/azurecore/src/azurecore.d.ts
vendored
@@ -17,6 +17,175 @@ 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
|
||||
*/
|
||||
@@ -76,7 +245,9 @@ 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>;
|
||||
@@ -106,7 +277,9 @@ declare module 'azurecore' {
|
||||
|
||||
export type GetSubscriptionsResult = { subscriptions: azureResource.AzureResourceSubscription[], errors: Error[] };
|
||||
export type GetResourceGroupsResult = { resourceGroups: azureResource.AzureResourceResourceGroup[], errors: Error[] };
|
||||
export type GetSqlManagedInstancesResult = { resources: azureResource.AzureGraphResource[], 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 GetSqlServersResult = { resources: azureResource.AzureGraphResource[], errors: Error[] };
|
||||
export type GetSqlVMServersResult = { resources: azureResource.AzureGraphResource[], errors: Error[] };
|
||||
export type GetStorageAccountResult = { resources: azureResource.AzureGraphResource[], errors: Error[] };
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user