Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd3fc5cea7 | ||
|
|
719bab7358 | ||
|
|
95f72e1248 | ||
|
|
f9a9f677ca | ||
|
|
dd0c5df25c | ||
|
|
4fc3244049 | ||
|
|
5faf830d46 | ||
|
|
d2cf82b85c | ||
|
|
fb0020c395 | ||
|
|
e55ea0b629 | ||
|
|
9234dbae70 | ||
|
|
27136eaa71 | ||
|
|
6554af1164 | ||
|
|
437d0e686d | ||
|
|
25b801826d | ||
|
|
123818dc70 | ||
|
|
dafcd046d7 | ||
|
|
accf9faf60 | ||
|
|
6bb237e78c | ||
|
|
bbb5808875 | ||
|
|
905a8f1951 | ||
|
|
f39961bbf5 | ||
|
|
2bf09011fb | ||
|
|
013aa2e570 | ||
|
|
953e35bcd6 | ||
|
|
e138b06fd3 | ||
|
|
2ef6b0549d | ||
|
|
1c49303432 | ||
|
|
f31b1e975d | ||
|
|
48d0087141 | ||
|
|
58cc9e56fc | ||
|
|
189a453e8b | ||
|
|
6fca90747c | ||
|
|
92f5a65c00 | ||
|
|
d8744e81bc | ||
|
|
136baf8260 | ||
|
|
ef47c163d0 | ||
|
|
39e5e3406b | ||
|
|
91d8d61afb | ||
|
|
b1d9153efe | ||
|
|
fda1f56ad1 | ||
|
|
fa18bfff01 | ||
|
|
72f7c8a92d | ||
|
|
838bc37243 | ||
|
|
866972c6c6 | ||
|
|
b19d76f249 | ||
|
|
35ccaeba56 | ||
|
|
bf134072e5 | ||
|
|
0bd152e0b5 | ||
|
|
928ca5409c | ||
|
|
0f846eab2c | ||
|
|
14ed74dd3d | ||
|
|
701d19bd18 | ||
|
|
240a544d82 | ||
|
|
b4f3741979 | ||
|
|
396bf0a43f | ||
|
|
3225ccda10 | ||
|
|
ad2c1ba2ad | ||
|
|
ea591df0bb | ||
|
|
72639bd6a3 | ||
|
|
31f0044c0f | ||
|
|
d0924f7e6d | ||
|
|
d2616acf36 | ||
|
|
dfd116bd56 | ||
|
|
830acef847 | ||
|
|
ee7c916494 | ||
|
|
46d477ea5b | ||
|
|
177b4af695 | ||
|
|
d2b8805605 | ||
|
|
39dedc68ec | ||
|
|
bd67756cda | ||
|
|
dd37fa4270 | ||
|
|
3c5c048152 | ||
|
|
dd6e4ef2d0 | ||
|
|
bce79f3488 | ||
|
|
74793a2399 | ||
|
|
6b120b053a | ||
|
|
1d0d62337e | ||
|
|
df8bfc42bb | ||
|
|
ff087738fc | ||
|
|
27567c88c9 | ||
|
|
9077a747d6 | ||
|
|
b359d9f525 | ||
|
|
ad7bd2e52d | ||
|
|
36aed9588b | ||
|
|
f8c81a29c4 | ||
|
|
c91771e152 | ||
|
|
3b7873302b | ||
|
|
d79bd54f0b | ||
|
|
51dc9a0e49 | ||
|
|
b49e55d400 | ||
|
|
33cb7e0a50 | ||
|
|
9edc90306e | ||
|
|
0a75e05b28 | ||
|
|
4a179ac3e3 | ||
|
|
42e32f9568 | ||
|
|
0223755b2a | ||
|
|
30a5f2a701 | ||
|
|
d2d351c9f7 | ||
|
|
e1696e9a4d | ||
|
|
25f015b179 | ||
|
|
9e263379a7 | ||
|
|
4c7ea161cd | ||
|
|
dc026c44a1 | ||
|
|
cb8b21c10c | ||
|
|
47c1beadcd | ||
|
|
8f7eb2abea | ||
|
|
c2b0f321de | ||
|
|
ba7b8b8824 | ||
|
|
9e612cf83d | ||
|
|
2154251c30 | ||
|
|
2d38266609 | ||
|
|
d414df4ca4 | ||
|
|
eab93cf6c5 | ||
|
|
3304def44a | ||
|
|
a4bf02b21b | ||
|
|
e0982da744 | ||
|
|
8abed1a809 | ||
|
|
a9a6518b23 | ||
|
|
1bafbe1ad2 | ||
|
|
a26074a3b8 | ||
|
|
90fd2f1447 | ||
|
|
e59690e703 | ||
|
|
bf06e9f654 | ||
|
|
84359d8e79 | ||
|
|
7801429530 | ||
|
|
dc9376a460 | ||
|
|
1f4076d8d4 | ||
|
|
f202d0f533 | ||
|
|
4344dcbb36 | ||
|
|
87d6bbae2a | ||
|
|
7a42bf456e | ||
|
|
a51f1eee8e | ||
|
|
5b72a916e4 | ||
|
|
42013a919f | ||
|
|
3918f82e14 | ||
|
|
6e79a2a133 | ||
|
|
088deaed3a | ||
|
|
7504e7710c | ||
|
|
bb7352ca54 | ||
|
|
b42ecbff36 | ||
|
|
5fd9f0216a | ||
|
|
942ea99459 | ||
|
|
990e2d6b9d | ||
|
|
f776ba0617 | ||
|
|
fb57965457 | ||
|
|
b99fce7d55 | ||
|
|
0a2d743ea2 | ||
|
|
fbd613084d | ||
|
|
4cd56f8ae5 | ||
|
|
c0d41c0a62 | ||
|
|
389c635030 | ||
|
|
d93a0c170c | ||
|
|
aa18f599ce | ||
|
|
aa7eb2e844 | ||
|
|
8e31adb9db | ||
|
|
f0feb9e563 | ||
|
|
44624e3820 | ||
|
|
225c94b853 | ||
|
|
bd1b8542eb | ||
|
|
c92e93ab1c | ||
|
|
1358727832 | ||
|
|
9bc84a2c59 | ||
|
|
7f00a83b19 | ||
|
|
19426cfd51 | ||
|
|
30c05eb834 | ||
|
|
f1d334b848 | ||
|
|
961f497b6b | ||
|
|
89f33f3714 | ||
|
|
4bad52f43a | ||
|
|
fd6cd3ddf7 | ||
|
|
adf7f8603b | ||
|
|
2a2e4350ee | ||
|
|
f563a4ef45 | ||
|
|
37b6ef6d3f | ||
|
|
96ff0ddf27 | ||
|
|
746862a897 | ||
|
|
5d6e65f206 | ||
|
|
6f9a464302 | ||
|
|
abf7866292 | ||
|
|
10c98c66e7 | ||
|
|
c1f7fe2ddd | ||
|
|
7f4586ed9e | ||
|
|
0b6049c8b0 | ||
|
|
2d347171a9 | ||
|
|
a085d4d4f7 | ||
|
|
7e2f3e3df3 | ||
|
|
3899daea7c | ||
|
|
3d1ee732b4 | ||
|
|
899d7beee0 | ||
|
|
4c7f3fad75 | ||
|
|
b8eb2c3c9b | ||
|
|
f5d612c8b5 | ||
|
|
4020f2d4e2 | ||
|
|
5794cfc7b2 | ||
|
|
1284a16978 | ||
|
|
f255a470cd | ||
|
|
f738e71657 | ||
|
|
beeee851ac | ||
|
|
eb365f0d39 | ||
|
|
57e614e8db | ||
|
|
738fa1f15f | ||
|
|
4c959ab297 | ||
|
|
e685295d04 | ||
|
|
204bd10e04 | ||
|
|
5a302b0bcc | ||
|
|
daa23f66c4 | ||
|
|
b06c396086 | ||
|
|
5555603a5c | ||
|
|
7c3b6d6d9e | ||
|
|
ce87d7728c | ||
|
|
e4da851172 | ||
|
|
e3433ec1f7 | ||
|
|
96f30d1b31 | ||
|
|
f4a48467e3 | ||
|
|
bada405511 | ||
|
|
96d7c055b0 | ||
|
|
a747614a74 | ||
|
|
84856407ee | ||
|
|
9bfd624713 | ||
|
|
b2a6e4d47f | ||
|
|
308240be89 | ||
|
|
c12edc7f73 | ||
|
|
44bd5ee1c6 | ||
|
|
8f3cccce32 | ||
|
|
1ef7d9b5a7 | ||
|
|
c445f95aa4 | ||
|
|
96db738f4b | ||
|
|
6181df4c69 | ||
|
|
376787fda1 | ||
|
|
80ffa856bd | ||
|
|
eb9fe573b8 | ||
|
|
e222d5b909 | ||
|
|
1c774aa783 | ||
|
|
04b51da60f | ||
|
|
28ddec6898 | ||
|
|
d987f844de | ||
|
|
b0d26fe1e6 | ||
|
|
e96131a933 | ||
|
|
3d58745bf1 | ||
|
|
9560148253 | ||
|
|
ec00efb656 | ||
|
|
ba34307918 | ||
|
|
28d8afae2e | ||
|
|
2e507be454 | ||
|
|
dbf3617224 | ||
|
|
200a7944e6 | ||
|
|
8dafc2b9a0 | ||
|
|
b7559c1f4e | ||
|
|
dd8e1864db | ||
|
|
d85a84980f | ||
|
|
4295966b7f | ||
|
|
a87c079ade | ||
|
|
b3604e3e40 | ||
|
|
6be8130329 | ||
|
|
ca3972c04f | ||
|
|
4d98729cde | ||
|
|
185af6f00e | ||
|
|
c5353d6e5e | ||
|
|
20a05dc96a | ||
|
|
30028b5ba0 | ||
|
|
3bafb708ea | ||
|
|
24d86a230a | ||
|
|
620f822504 | ||
|
|
93a57f26ee | ||
|
|
40ec4493ea | ||
|
|
de83d59bde | ||
|
|
a937c5e8de | ||
|
|
b820bb19d7 | ||
|
|
36db2d3f1d | ||
|
|
5786b86521 | ||
|
|
c6a5f93b44 | ||
|
|
8814e2eacb | ||
|
|
9b1e4a8291 | ||
|
|
01c2468f86 | ||
|
|
af705718ff | ||
|
|
e22ef12529 | ||
|
|
62510d9243 | ||
|
|
0fd25a6bb4 | ||
|
|
a268a7b75e | ||
|
|
3dded0a7bc | ||
|
|
ab3140be0a | ||
|
|
0fcd58c080 | ||
|
|
52f0d9f9e8 | ||
|
|
6585fa8e41 | ||
|
|
bef9b3b276 | ||
|
|
af411b854a | ||
|
|
a697dba5b9 | ||
|
|
1203081ec7 | ||
|
|
2db63ae2f8 | ||
|
|
9dd1617b57 | ||
|
|
834058ff45 | ||
|
|
0f41a0a576 | ||
|
|
1025cbfb87 | ||
|
|
ca3c1c7623 | ||
|
|
69d127a89e | ||
|
|
b4d9cd5bb7 | ||
|
|
4b1327db24 | ||
|
|
2f2ceed24e | ||
|
|
de0f20809d | ||
|
|
b1f5857cac | ||
|
|
ddbd1ca163 | ||
|
|
777e40fff8 | ||
|
|
68460331f5 | ||
|
|
32e9fe8b6a | ||
|
|
b3a2460b3f | ||
|
|
3260bf1b2a | ||
|
|
e69c6456d1 | ||
|
|
f8b3792e62 | ||
|
|
01c34c0a79 | ||
|
|
825751239f | ||
|
|
2465739060 | ||
|
|
e6834d2033 | ||
|
|
924100df4c | ||
|
|
1ac7d1e08b | ||
|
|
7becccd628 | ||
|
|
dff5cac4fe | ||
|
|
ecd0c1de20 | ||
|
|
878cb17b62 | ||
|
|
706981c231 | ||
|
|
d212cf7c40 | ||
|
|
19b6e5154f | ||
|
|
89263918ea | ||
|
|
e8fc6d163b | ||
|
|
34a670f8d7 | ||
|
|
6a46e2dd57 | ||
|
|
5e0fa7c744 | ||
|
|
474ce17a13 | ||
|
|
f00eed8633 | ||
|
|
42bcefa068 | ||
|
|
29bde998ed | ||
|
|
e29a83ec25 | ||
|
|
ddfcbde7d4 | ||
|
|
42e31d3ece | ||
|
|
cd8480bc67 | ||
|
|
06dc05be08 | ||
|
|
d3fda81af3 | ||
|
|
f58d345b63 | ||
|
|
f35382a4e0 | ||
|
|
8087430dd2 | ||
|
|
2a5f1f3668 | ||
|
|
a889cb30e0 | ||
|
|
e411c0ab7b | ||
|
|
a136371c1f | ||
|
|
83fa213854 | ||
|
|
750cc1ecae | ||
|
|
e25a854d24 | ||
|
|
3b32195271 | ||
|
|
f9411794c4 | ||
|
|
c5fdbfd62d | ||
|
|
f12ae90aba | ||
|
|
20c4bbcee2 | ||
|
|
f3232f34c2 | ||
|
|
8fedb23399 | ||
|
|
4ca519ce62 | ||
|
|
6d4768272a | ||
|
|
e8cc22879f | ||
|
|
b287683a61 | ||
|
|
ea43ee99d6 | ||
|
|
48db68e7ac | ||
|
|
7807b7a280 | ||
|
|
ad0e223742 | ||
|
|
c9a74f2800 | ||
|
|
570ed61edb | ||
|
|
835b302c19 | ||
|
|
a0fad9874a | ||
|
|
cb895024e0 | ||
|
|
2e94ebdb64 | ||
|
|
ae1e6d2748 | ||
|
|
c72d4f35a0 | ||
|
|
9742d824c4 | ||
|
|
639aa47e19 | ||
|
|
5fa0d11aa9 | ||
|
|
fed32aa57b | ||
|
|
1e2134bd7e | ||
|
|
064375d457 | ||
|
|
deff38de56 | ||
|
|
15950d3a09 | ||
|
|
c4539b8e30 | ||
|
|
904864b14e | ||
|
|
390d179917 | ||
|
|
3b6a421af2 | ||
|
|
cc8d4fe952 | ||
|
|
dfa62dd04b | ||
|
|
520020f2c6 | ||
|
|
31f1ed8dd4 | ||
|
|
5abd4f1f4f | ||
|
|
862ce645cf | ||
|
|
9eccbb91db | ||
|
|
343e1d2193 | ||
|
|
9a11925216 | ||
|
|
f7f1041daa | ||
|
|
1cbf3b227e | ||
|
|
faf4ecfac6 | ||
|
|
ca4aad0a2b | ||
|
|
ed569306bb | ||
|
|
65abd7a4e3 | ||
|
|
422cff4547 | ||
|
|
7695135302 | ||
|
|
5be4868e9a | ||
|
|
fce97fd57a | ||
|
|
af8655ec45 | ||
|
|
daed714b98 | ||
|
|
584e62cff3 | ||
|
|
6d2f1dcbbf | ||
|
|
5e7b74a7ed | ||
|
|
6de20b03ef | ||
|
|
2869020e65 | ||
|
|
40c8ceeca6 | ||
|
|
0623d4ad47 | ||
|
|
0b9c235dd2 | ||
|
|
e16b4e68c8 | ||
|
|
078e36f9b8 | ||
|
|
8874d0be86 | ||
|
|
b59afd3455 | ||
|
|
85a2684532 | ||
|
|
f9a7864b07 | ||
|
|
371301b4ba | ||
|
|
9c10d79661 | ||
|
|
018d879a0a | ||
|
|
b64f1876dc | ||
|
|
055607b3dd | ||
|
|
8a7fa26cb5 | ||
|
|
9cac16fe6a | ||
|
|
61297dba02 | ||
|
|
2f8a53e3ea | ||
|
|
96b9a20844 | ||
|
|
443f230251 | ||
|
|
86ee79e6bf | ||
|
|
b24e5a2c22 | ||
|
|
8c6b1aa69a | ||
|
|
36c2aee271 | ||
|
|
b049635d28 | ||
|
|
42dd029611 | ||
|
|
e02c50aef8 | ||
|
|
d9cf112694 | ||
|
|
a4456f50cd | ||
|
|
a358061f22 | ||
|
|
f155e8ad44 | ||
|
|
c930bb235b | ||
|
|
543cf4697f | ||
|
|
66e9e971a1 | ||
|
|
a961b5e4be | ||
|
|
f8796b362e | ||
|
|
415f638028 | ||
|
|
4523615829 | ||
|
|
61214af01a | ||
|
|
2f0bf5dc17 | ||
|
|
6e71712092 | ||
|
|
7e56e1cccf | ||
|
|
1e3f533d83 | ||
|
|
57568f9a8c | ||
|
|
7c1814118d | ||
|
|
ca50a963fd | ||
|
|
6e188c6996 | ||
|
|
0095c5c666 | ||
|
|
bdd7f303c4 | ||
|
|
f0383522d8 | ||
|
|
a32cc13171 | ||
|
|
dbda8c5674 | ||
|
|
186873d9d4 | ||
|
|
37482c0b36 | ||
|
|
ccdc5a03ac | ||
|
|
2ae24c4627 | ||
|
|
b9e9fd21ee | ||
|
|
6eb83b2a25 | ||
|
|
70aceb1699 | ||
|
|
5e208c4115 | ||
|
|
390f051e43 | ||
|
|
ab0f1fd549 | ||
|
|
788df7b16d | ||
|
|
01a2d8e9a6 | ||
|
|
e842eb001d | ||
|
|
f195e5dc9d | ||
|
|
7f7f0f1859 | ||
|
|
b494a703eb | ||
|
|
0f52b86364 | ||
|
|
1b31188d5e | ||
|
|
30e7268297 | ||
|
|
6956b30fd6 | ||
|
|
98d19ef591 | ||
|
|
f0ffcea424 | ||
|
|
6454fb7061 | ||
|
|
c0d7af7899 | ||
|
|
2424395415 |
No files matched your search
@@ -13,7 +13,10 @@ project {
|
|||||||
"**/node_modules/**",
|
"**/node_modules/**",
|
||||||
"**/dist/**",
|
"**/dist/**",
|
||||||
"**/coverage/**",
|
"**/coverage/**",
|
||||||
|
"**/pnpm-lock.yaml",
|
||||||
|
"**/pnpm-workspace.yaml",
|
||||||
"ui/desktop/electron-app/out/**",
|
"ui/desktop/electron-app/out/**",
|
||||||
"ui/desktop/electron-app/ember-dist/**",
|
"ui/desktop/electron-app/ember-dist/**",
|
||||||
|
"e2e-tests/api-client/**"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -11,21 +11,57 @@ assignees: ''
|
|||||||
|
|
||||||
For questions, the best place to get answers is on our [discussion forum](https://discuss.hashicorp.com/c/boundary), as they will get more visibility from experienced users than the issue tracker.
|
For questions, the best place to get answers is on our [discussion forum](https://discuss.hashicorp.com/c/boundary), as they will get more visibility from experienced users than the issue tracker.
|
||||||
|
|
||||||
Please note: We take Boundary's security and our users' trust very seriously. If you believe you have found a security issue in Boundary, please responsibly disclose by contacting us at security@hashicorp.com. Our PGP key is available at [our security page](https://www.hashicorp.com/security/).
|
Please note: We take Boundary's security and our users' trust very seriously. If you believe you have found a security issue in Boundary, please responsibly disclose this by contacting us at security@hashicorp.com. Our PGP key is available on [our security page](https://www.hashicorp.com/security/).
|
||||||
|
|
||||||
|
Logging is available on Boundary Desktop Client version >= 2.1.0
|
||||||
|
|
||||||
|
Logs are found at:
|
||||||
|
|
||||||
|
Linux: ~/.config/Boundary/logs/desktop-client.log
|
||||||
|
MacOS: ~/Library/Logs/Boundary/desktop-client.log
|
||||||
|
Windows: $env:USERPROFILE\AppData\Roaming\Boundary\logs\desktop-client.log
|
||||||
|
|
||||||
|
If requests are taking longer than expected, please change the logging level to DEBUG. This will log the cache daemon request times as well.
|
||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
**Describe the bug**
|
## :bug: Bug Report
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**To Reproduce**
|
### **Description**
|
||||||
Steps to reproduce the behavior:
|
|
||||||
1. Run `boundary authenticate ...`
|
|
||||||
2. Run `boundary connect ...`
|
|
||||||
3. See error
|
|
||||||
|
|
||||||
**Expected behavior**
|
_A clear and concise description of the bug._
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Additional context**
|
### **Steps to Reproduce**
|
||||||
Add any other context about the problem here.
|
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '...'
|
||||||
|
3. Scroll down to '...'
|
||||||
|
4. See the error
|
||||||
|
|
||||||
|
## **Expected behavior**
|
||||||
|
|
||||||
|
_A clear description of what you expected to happen._
|
||||||
|
|
||||||
|
## **Actual behavior**
|
||||||
|
|
||||||
|
_A clear description of what actually happened._
|
||||||
|
|
||||||
|
## **Screenshots (if applicable)**
|
||||||
|
|
||||||
|
_Add screenshots or screen recordings to help explain the issue._
|
||||||
|
|
||||||
|
## **Environment**
|
||||||
|
|
||||||
|
- **OS and Version:** (e.g., Windows 10 (22H2), macOS (14.7.2))
|
||||||
|
- **Architecture:** (e.g., arm64, x86_64)
|
||||||
|
- **Browser/Version:** (if applicable)
|
||||||
|
- **Desktop Client Version:** (if applicable)
|
||||||
|
- **Controller/worker versions:** (if applicable)
|
||||||
|
|
||||||
|
## **Logs & Console Output (if applicable)**
|
||||||
|
|
||||||
|
_Paste any relevant error logs or browser console output here._
|
||||||
|
|
||||||
|
## **Additional Context**
|
||||||
|
|
||||||
|
_Any other details that might be relevant._
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "monthly"
|
||||||
@@ -12,9 +12,22 @@
|
|||||||
## Checklist
|
## Checklist
|
||||||
<!-- strikethrough the checklist item that is not relevant to your change -->
|
<!-- strikethrough the checklist item that is not relevant to your change -->
|
||||||
|
|
||||||
|
<!-- If you are merging a long lived branch, major feature, or UI altering changes,
|
||||||
|
please add the a11y-tests label. Other cases, like a dependency change or
|
||||||
|
translation update likely does not need an a11y audit -->
|
||||||
|
|
||||||
- [ ] I have added before and after screenshots for UI changes
|
- [ ] I have added before and after screenshots for UI changes
|
||||||
- [ ] I have added JSON response output for API changes
|
- [ ] I have added JSON response output for API changes
|
||||||
- [ ] I have added steps to reproduce and test for bug fixes in the description
|
- [ ] I have added steps to reproduce and test for bug fixes in the description
|
||||||
- [ ] I have commented on my code, particularly in hard-to-understand areas
|
- [ ] I have commented on my code, particularly in hard-to-understand areas
|
||||||
- [ ] My changes generate no new warnings
|
- [ ] My changes generate no new warnings
|
||||||
- [ ] I have added tests that prove my fix is effective or that my feature works
|
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||||
|
- [ ] I have added `a11y-tests` label to run a11y audit tests if needed
|
||||||
|
|
||||||
|
## PCI review checklist
|
||||||
|
|
||||||
|
<!-- heimdall_github_prtemplate:grc-pci_dss-2024-01-05 -->
|
||||||
|
- [ ] I have documented a clear reason for, and description of, the change I am making.
|
||||||
|
- [ ] If applicable, I've documented a plan to revert these changes if they require more than reverting the pull request.
|
||||||
|
- [ ] If applicable, I've documented the impact of any changes to security controls.
|
||||||
|
Examples of changes to security controls include using new access control methods, adding or removing logging pipelines, etc.
|
||||||
@@ -27,7 +27,6 @@ echo "Trying to merge the latest oss commit - ${latest_oss_commit} from oss/${BR
|
|||||||
if ! errors=$(git merge -m "Merge Boundary UI OSS branch '${BRANCH}' at commit ${latest_oss_commit}" "${latest_oss_commit}"); then
|
if ! errors=$(git merge -m "Merge Boundary UI OSS branch '${BRANCH}' at commit ${latest_oss_commit}" "${latest_oss_commit}"); then
|
||||||
# Here we can try to auto resolve conflicts
|
# Here we can try to auto resolve conflicts
|
||||||
# This logic will vary from project to project. The below is just an example.
|
# This logic will vary from project to project. The below is just an example.
|
||||||
# We'll try to resolve package.json and yarn.lock conflicts by taking the OSS changes
|
|
||||||
conflicts=$(git diff --name-only --diff-filter=U)
|
conflicts=$(git diff --name-only --diff-filter=U)
|
||||||
printf "Found conflicts:\n${conflicts}\n"
|
printf "Found conflicts:\n${conflicts}\n"
|
||||||
# Resolve package.json conflicts by taking oss
|
# Resolve package.json conflicts by taking oss
|
||||||
@@ -36,11 +35,12 @@ if ! errors=$(git merge -m "Merge Boundary UI OSS branch '${BRANCH}' at commit $
|
|||||||
git checkout --theirs package.json
|
git checkout --theirs package.json
|
||||||
git add package.json
|
git add package.json
|
||||||
fi
|
fi
|
||||||
# Resolve yarn.lock conflicts by taking oss
|
# Resolve pnpm-lock.yaml conflicts by re-running pnpm install:
|
||||||
if echo "$conflicts" | grep -q yarn.lock; then
|
# https://pnpm.io/git#merge-conflicts
|
||||||
echo "Checking out oss yarn.lock..."
|
if echo "$conflicts" | grep -q pnpm-lock.yaml; then
|
||||||
git checkout --theirs yarn.lock
|
echo "Re-running pnpm install to fix conflicts by regenerating pnpm-lock.yaml..."
|
||||||
git add yarn.lock
|
pnpm install
|
||||||
|
git add pnpm-lock.yaml
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# If we can commit all the changes without conflict, we have resolved everything automatically
|
# If we can commit all the changes without conflict, we have resolved everything automatically
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
name: A11y Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [labeled, opened, synchronize, reopened]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
a11y-test:
|
||||||
|
name: Run A11y Tests
|
||||||
|
if: contains(github.event.pull_request.labels.*.name, 'a11y-tests')
|
||||||
|
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||||
|
timeout-minutes: 40
|
||||||
|
steps:
|
||||||
|
- uses: browser-actions/setup-chrome@b94431e051d1c52dcbe9a7092a4f10f827795416 # v2.1.0
|
||||||
|
with:
|
||||||
|
install-dependencies: true
|
||||||
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
- run: pnpm install
|
||||||
|
- name: Run a11y tests
|
||||||
|
env:
|
||||||
|
# this will generate the test reports but causes tests to pass even when there a11y failures
|
||||||
|
ENABLE_A11Y_MIDDLEWARE_REPORTER: true
|
||||||
|
run: pnpm test-a11y
|
||||||
|
- name: Consolidate ember a11y reports
|
||||||
|
run: |
|
||||||
|
mkdir ember-a11y-reports
|
||||||
|
jq -s 'map(.[])' ./ui/admin/ember-a11y-report/*.json > ember-a11y-reports/admin.json
|
||||||
|
jq -s 'map(.[])' ./ui/desktop/ember-a11y-report/*.json > ember-a11y-reports/desktop.json
|
||||||
|
- name: Output ember a11y test report summary (admin)
|
||||||
|
run: 'cat ember-a11y-reports/admin.json | jq "map({moduleName,testName,routes,urls,violations: .violations | map({id, nodes: .nodes | map({failureSummary, targets: .[].[]?.relatedNodes?.[]?.target? })}) })"'
|
||||||
|
- name: Output ember a11y test report summary (desktop)
|
||||||
|
run: 'cat ember-a11y-reports/desktop.json | jq "map({moduleName,testName,routes,urls,violations: .violations | map({id, nodes: .nodes | map({failureSummary, targets: .[].[]?.relatedNodes?.[]?.target? })}) })"'
|
||||||
|
- name: Upload ember a11y reports
|
||||||
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
|
with:
|
||||||
|
name: ember-a11y-reports
|
||||||
|
path: ./ember-a11y-reports
|
||||||
|
- name: Report a11y test results status
|
||||||
|
if: always()
|
||||||
|
# fail CI when the combined array length of admin and desktop a11y reports is not 0
|
||||||
|
run: |
|
||||||
|
EMBER_A11Y_FAILURE_COUNT=$(jq -s 'map(.[]) | length' ember-a11y-reports/admin.json ember-a11y-reports/desktop.json)
|
||||||
|
echo "$EMBER_A11Y_FAILURE_COUNT ember-a11y test failures"
|
||||||
|
if [ "$EMBER_A11Y_FAILURE_COUNT" != '0' ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -9,91 +9,86 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
dependencies:
|
dependencies:
|
||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
|
||||||
id: node-modules-cache
|
|
||||||
with:
|
with:
|
||||||
path: "**/node_modules"
|
run_install: false
|
||||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
restore-keys: |
|
|
||||||
node-modules-
|
|
||||||
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
- run: yarn install
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
- run: pnpm install --frozen-lockfile
|
||||||
build-admin-ui-oss:
|
build-admin-ui-oss:
|
||||||
name: Build Admin UI OSS
|
name: Build Admin UI OSS
|
||||||
needs: [dependencies]
|
needs: [dependencies]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
|
||||||
id: node-modules-cache
|
|
||||||
with:
|
with:
|
||||||
path: "**/node_modules"
|
run_install: false
|
||||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
node-modules-
|
|
||||||
- name: Build
|
- name: Build
|
||||||
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
- run: yarn run build:ui:admin:oss
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
- run: pnpm install --offline
|
||||||
|
- run: pnpm run build:ui:admin:oss
|
||||||
- name: Upload artifact admin-ui
|
- name: Upload artifact admin-ui
|
||||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: admin-ui
|
name: admin-ui
|
||||||
path: ./ui/admin/dist/
|
path: ./ui/admin/dist/
|
||||||
- name: Upload artifact OSS admin UI
|
- name: Upload artifact OSS admin UI
|
||||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: admin-ui-oss
|
name: admin-ui-oss
|
||||||
path: ./ui/admin/dist/
|
path: ./ui/admin/dist/
|
||||||
build-admin-ui-enterprise:
|
build-admin-ui-enterprise:
|
||||||
name: Build Admin UI Enterprise
|
name: Build Admin UI Enterprise
|
||||||
needs: [dependencies]
|
needs: [dependencies]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
|
||||||
id: node-modules-cache
|
|
||||||
with:
|
with:
|
||||||
path: "**/node_modules"
|
run_install: false
|
||||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
node-modules-
|
|
||||||
- name: Build
|
- name: Build
|
||||||
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
- run: yarn run build:ui:admin:enterprise
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
- run: pnpm install --offline
|
||||||
|
- run: pnpm run build:ui:admin:enterprise
|
||||||
- name: Upload artifact enterprise admin UI
|
- name: Upload artifact enterprise admin UI
|
||||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: admin-ui-ent
|
name: admin-ui-ent
|
||||||
path: ./ui/admin/dist/
|
path: ./ui/admin/dist/
|
||||||
build-admin-ui-hcp:
|
build-admin-ui-hcp:
|
||||||
name: Build Admin UI HCP
|
name: Build Admin UI HCP
|
||||||
needs: [dependencies]
|
needs: [dependencies]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
|
||||||
id: node-modules-cache
|
|
||||||
with:
|
with:
|
||||||
path: "**/node_modules"
|
run_install: false
|
||||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
node-modules-
|
|
||||||
- name: Build
|
- name: Build
|
||||||
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
- run: yarn run build:ui:admin:hcp
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
- run: pnpm install --offline
|
||||||
|
- run: pnpm run build:ui:admin:hcp
|
||||||
- name: Upload artifact HCP admin UI
|
- name: Upload artifact HCP admin UI
|
||||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: admin-ui-hcp
|
name: admin-ui-hcp
|
||||||
path: ./ui/admin/dist/
|
path: ./ui/admin/dist/
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
name: "Trigger Build Artifacts for Testing"
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
branch:
|
|
||||||
description: Please use tag-like format n.n.n-someword (1.111.11-beta)
|
|
||||||
required: true
|
|
||||||
product_version:
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
description: The version number belonging to the oss binary to be used
|
|
||||||
commit:
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
description: The git SHA of the oss artifacts to be used
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
trigger-gha-build-artifacts-for-testing:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Release description
|
|
||||||
if: github.event_name == 'workflow_dispatch'
|
|
||||||
run: |
|
|
||||||
echo "Triggering Build Artifacts for Testing using BRANCH and TEST_TAG ${{ github.event.inputs.branch }}"
|
|
||||||
- name: Trigger Build Artifacts for Testing in boundary-ui-releases
|
|
||||||
run: |
|
|
||||||
export GITHUB_TOKEN="${{ secrets.ELEVATED_GITHUB_TOKEN }}"
|
|
||||||
gh workflow run build-artifacts-for-testing.yml \
|
|
||||||
--repo hashicorp/boundary-ui-releases \
|
|
||||||
--ref main \
|
|
||||||
-f TAG="${{ inputs.branch }}" \
|
|
||||||
-f PRODUCT_VERSION="${{ inputs.product_version }}" \
|
|
||||||
-f COMMIT="${{ inputs.commit }}" \
|
|
||||||
@@ -17,7 +17,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
name: Analyze
|
name: Analyze
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@@ -27,18 +27,18 @@ jobs:
|
|||||||
language: ['javascript']
|
language: ['javascript']
|
||||||
# Learn more...
|
# Learn more...
|
||||||
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
|
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@5618c9fc1e675841ca52c1c6b1304f5255a905a0 # codeql-bundle-v2.19.0
|
uses: github/codeql-action/init@70df9def86d22bf0ea4e7f8b956e7b92e7c1ea22 # codeql-bundle-v2.20.7
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
# By default, queries listed here will override any specified in a config file.
|
# By default, queries listed here will override any specified in a config file.
|
||||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
|
|
||||||
@@ -54,4 +54,4 @@ jobs:
|
|||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@5618c9fc1e675841ca52c1c6b1304f5255a905a0 # codeql-bundle-v2.19.0
|
uses: github/codeql-action/analyze@70df9def86d22bf0ea4e7f8b956e7b92e7c1ea22 # codeql-bundle-v2.20.7
|
||||||
@@ -2,14 +2,18 @@ name: Add Copyright Headers
|
|||||||
on: pull_request
|
on: pull_request
|
||||||
jobs:
|
jobs:
|
||||||
copywrite:
|
copywrite:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
# Ref and repository aren't normally needed, this is a workaround for a bug in auto commit
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
- uses: hashicorp/setup-copywrite@32638da2d4e81d56a0764aa1547882fc4d209636 # v1.1.3
|
- uses: hashicorp/setup-copywrite@32638da2d4e81d56a0764aa1547882fc4d209636 # v1.1.3
|
||||||
- name: Add missing Copyright Headers
|
- name: Add missing Copyright Headers
|
||||||
run: copywrite headers
|
run: copywrite headers
|
||||||
- uses: stefanzweifel/git-auto-commit-action@8621497c8c39c72f3e2a999a26b4ca1b5058a842 # v5.0.1
|
- uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0
|
||||||
with:
|
with:
|
||||||
commit_message: Add missing copyright headers
|
commit_message: Add missing copyright headers
|
||||||
@@ -11,7 +11,7 @@ name: Jira Sync
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sync:
|
sync:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||||
name: Jira sync
|
name: Jira sync
|
||||||
steps:
|
steps:
|
||||||
- name: Check if community user
|
- name: Check if community user
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
triage:
|
triage:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0
|
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
|
||||||
with:
|
with:
|
||||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
@@ -4,71 +4,71 @@ jobs:
|
|||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
|
||||||
id: node-modules-cache
|
|
||||||
with:
|
with:
|
||||||
path: '**/node_modules'
|
run_install: false
|
||||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
restore-keys: |
|
|
||||||
node-modules-
|
|
||||||
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
- run: yarn install
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
- run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
compliance:
|
compliance:
|
||||||
name: License Compliance
|
name: License Compliance
|
||||||
needs: [dependencies]
|
needs: [dependencies]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
|
||||||
id: node-modules-cache
|
|
||||||
with:
|
with:
|
||||||
path: '**/node_modules'
|
run_install: false
|
||||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
restore-keys: |
|
|
||||||
node-modules-
|
|
||||||
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
- run: yarn run compliance:licenses
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
- run: pnpm install --offline
|
||||||
|
- run: pnpm run compliance:licenses
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
name: Lint
|
name: Lint
|
||||||
needs: [dependencies, compliance]
|
needs: [dependencies, compliance]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
|
||||||
id: node-modules-cache
|
|
||||||
with:
|
with:
|
||||||
path: '**/node_modules'
|
run_install: false
|
||||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
restore-keys: |
|
|
||||||
node-modules-
|
|
||||||
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
- run: yarn lint
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
- run: pnpm install --offline
|
||||||
|
- run: pnpm lint
|
||||||
|
|
||||||
test:
|
test:
|
||||||
name: Test
|
name: Test
|
||||||
needs: [lint]
|
needs: [lint]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
- uses: browser-actions/setup-chrome@b94431e051d1c52dcbe9a7092a4f10f827795416 # v2.1.0
|
||||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
|
||||||
id: node-modules-cache
|
|
||||||
with:
|
with:
|
||||||
path: '**/node_modules'
|
install-dependencies: true
|
||||||
key: node-modules-${{ hashFiles('**/yarn.lock') }}
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
- run: yarn test
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
- run: pnpm install --offline
|
||||||
|
- run: pnpm test
|
||||||
@@ -10,7 +10,7 @@ jobs:
|
|||||||
trigger-oss-merge:
|
trigger-oss-merge:
|
||||||
# Run this only on merge events in OSS repo
|
# Run this only on merge events in OSS repo
|
||||||
if: ${{ github.event.pull_request.merged && github.repository == 'hashicorp/boundary-ui' }}
|
if: ${{ github.event.pull_request.merged && github.repository == 'hashicorp/boundary-ui' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Trigger Merge
|
- name: Trigger Merge
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ on:
|
|||||||
types: [ oss-merge ]
|
types: [ oss-merge ]
|
||||||
jobs:
|
jobs:
|
||||||
oss-merge:
|
oss-merge:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.ELEVATED_GITHUB_TOKEN }}
|
token: ${{ secrets.ELEVATED_GITHUB_TOKEN }}
|
||||||
- run: |
|
- run: |
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
name: Test E2E Admin UI
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [labeled, opened, synchronize, reopened]
|
||||||
|
paths:
|
||||||
|
- 'ui/admin/**'
|
||||||
|
- 'e2e-tests/*'
|
||||||
|
- 'e2e-tests/api-client/**'
|
||||||
|
- 'e2e-tests/helpers/**'
|
||||||
|
- 'e2e-tests/admin/**'
|
||||||
|
- '.github/workflows/test-e2e-admin-ui.yaml'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
boundary-enterprise-branch:
|
||||||
|
description: 'Branch of boundary-enterprise to test against (default to main)'
|
||||||
|
required: true
|
||||||
|
default: 'main'
|
||||||
|
type: string
|
||||||
|
boundary-branch:
|
||||||
|
description: 'Branch of boundary to test against (default to main)'
|
||||||
|
required: true
|
||||||
|
default: 'main'
|
||||||
|
type: string
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: test
|
||||||
|
runs-on: ${{ fromJSON(vars.RUNNER_UBUNTU_22) }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- boundary_edition: community
|
||||||
|
github_repo: hashicorp/boundary
|
||||||
|
folder_name: boundary
|
||||||
|
boundary_edition_short: oss
|
||||||
|
enos_scenario: e2e_ui_docker
|
||||||
|
test_command: admin:ce:docker
|
||||||
|
boundary_branch: ${{ github.event.inputs.boundary-branch || 'main' }}
|
||||||
|
- boundary_edition: enterprise
|
||||||
|
github_repo: hashicorp/boundary-enterprise
|
||||||
|
folder_name: boundary-enterprise
|
||||||
|
boundary_edition_short: ent
|
||||||
|
enos_scenario: e2e_ui_docker_ent
|
||||||
|
test_command: admin:ent:docker
|
||||||
|
boundary_branch: ${{ github.event.inputs.boundary-enterprise-branch || 'main' }}
|
||||||
|
fail-fast: false
|
||||||
|
env:
|
||||||
|
ENOS_VAR_e2e_debug_no_run: true
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up pnpm
|
||||||
|
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Set up Node
|
||||||
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: "pnpm"
|
||||||
|
cache-dependency-path: |
|
||||||
|
pnpm-lock.yaml
|
||||||
|
ui/desktop/electron-app/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Set up Terraform
|
||||||
|
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd
|
||||||
|
|
||||||
|
- name: Set up Enos
|
||||||
|
uses: hashicorp/action-setup-enos@39a8f388434bea5eeba6649b99aa9949a55a1150
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Install Vault CLI
|
||||||
|
run: |
|
||||||
|
wget https://releases.hashicorp.com/vault/1.12.2/vault_1.12.2_linux_amd64.zip -O /tmp/vault.zip
|
||||||
|
unzip /tmp/vault.zip -d /usr/local/bin
|
||||||
|
|
||||||
|
- name: GPG setup
|
||||||
|
run: |
|
||||||
|
# Create a GPG key
|
||||||
|
export KEY_PW=boundary
|
||||||
|
gpg --generate-key --batch <<eoGpgConf
|
||||||
|
%echo Started!
|
||||||
|
Key-Type: RSA
|
||||||
|
Key-Length: default
|
||||||
|
Subkey-Type: RSA
|
||||||
|
Name-Real: boundary
|
||||||
|
Name-Comment: default
|
||||||
|
Name-Email: default
|
||||||
|
Expire-Date: 0
|
||||||
|
Passphrase: $KEY_PW
|
||||||
|
%commit
|
||||||
|
%echo Done.
|
||||||
|
eoGpgConf
|
||||||
|
|
||||||
|
# Enable gpg-preset-passphrase so that key passwords can be saved
|
||||||
|
echo "allow-preset-passphrase" >> ~/.gnupg/gpg-agent.conf
|
||||||
|
gpg-connect-agent reloadagent /bye &>/dev/null
|
||||||
|
|
||||||
|
# Get information about the created keys
|
||||||
|
export lines=$(gpg --list-secret-keys --with-colons --with-keygrip)
|
||||||
|
export KEY_ID=""
|
||||||
|
while read -r line
|
||||||
|
do
|
||||||
|
# Save the first key id to be used later
|
||||||
|
if [[ $line =~ "fpr"* ]]; then
|
||||||
|
if [[ $KEY_ID == "" ]]; then
|
||||||
|
KEY_ID="$(echo "$line" | sed -r 's/fpr|://g')"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Cache the passphrases for the keys so passwords do not need to be entered
|
||||||
|
if [[ $line =~ "grp"* ]]; then
|
||||||
|
export KEYGRIP_ID="$(echo "$line" | sed -r 's/grp|://g')"
|
||||||
|
/usr/lib/gnupg/gpg-preset-passphrase --preset -P $KEY_PW $KEYGRIP_ID
|
||||||
|
fi
|
||||||
|
done <<< $lines
|
||||||
|
|
||||||
|
# Trust the key
|
||||||
|
touch /tmp/test.txt
|
||||||
|
gpg -a --encrypt -r $KEY_ID --trust-model always --batch --yes /tmp/test.txt
|
||||||
|
echo "trusted-key $KEY_ID" >> ~/.gnupg/gpg.conf
|
||||||
|
|
||||||
|
# Initialize the password store
|
||||||
|
pass init $KEY_ID &>/dev/null
|
||||||
|
|
||||||
|
- name: Checkout ${{ matrix.folder_name }} repo
|
||||||
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
with:
|
||||||
|
repository: ${{ matrix.github_repo }}
|
||||||
|
path: support/src/${{ matrix.folder_name }}
|
||||||
|
token: ${{ secrets.ELEVATED_GITHUB_TOKEN }}
|
||||||
|
ref: ${{ matrix.boundary_branch }}
|
||||||
|
|
||||||
|
- name: GH fix for localhost resolution
|
||||||
|
run: |
|
||||||
|
cat /etc/hosts && echo "-----------"
|
||||||
|
sudo sed -i 's/::1 *localhost ip6-localhost ip6-loopback/::1 ip6 -localhost ip6-loopback/g' /etc/hosts
|
||||||
|
cat /etc/hosts
|
||||||
|
ssh -V
|
||||||
|
|
||||||
|
echo "127.0.0.1 localhost boundary" | sudo tee -a /etc/hosts
|
||||||
|
echo "127.0.0.1 localhost worker" | sudo tee -a /etc/hosts
|
||||||
|
echo "127.0.0.1 localhost vault" | sudo tee -a /etc/hosts
|
||||||
|
|
||||||
|
- name: Set git config
|
||||||
|
run: |
|
||||||
|
git config --global url."https://oauth2:${{ secrets.ELEVATED_GITHUB_TOKEN }}@github.com".insteadOf "https://github.com"
|
||||||
|
|
||||||
|
- name: Install node dependencies
|
||||||
|
run: |
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
cd e2e-tests
|
||||||
|
pnpm playwright install --with-deps
|
||||||
|
|
||||||
|
- name: Set up test infra
|
||||||
|
id: infra
|
||||||
|
run: |
|
||||||
|
ssh-keygen -N '' -t ed25519 -f ~/.ssh/github_enos
|
||||||
|
mkdir -p ./enos/terraform-plugin-cache
|
||||||
|
echo ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} > ./support/src/${{matrix.folder_name}}/internal/ui/VERSION
|
||||||
|
export ENOS_VAR_ui_build_override="oss" && \
|
||||||
|
export ENOS_VAR_boundary_edition=${{ matrix.boundary_edition_short }} && \
|
||||||
|
export ENOS_VAR_enos_user=$GITHUB_ACTOR && \
|
||||||
|
export ENOS_VAR_aws_ssh_private_key_path=~/.ssh/github_enos && \
|
||||||
|
export ENOS_VAR_boundary_license="${{ secrets.BOUNDARY_ENT_LICENSE }}" && \
|
||||||
|
enos scenario launch --timeout 60m0s --chdir ./support/src/${{matrix.folder_name}}/enos ${{matrix.enos_scenario}} builder:local
|
||||||
|
|
||||||
|
- name: Move boundary binary
|
||||||
|
run: |
|
||||||
|
mv ./support/src/${{matrix.folder_name}}/bin/boundary /usr/local/bin/
|
||||||
|
boundary version
|
||||||
|
|
||||||
|
- name: Run Tests
|
||||||
|
run: |
|
||||||
|
source <(bash ./support/src/${{matrix.folder_name}}/enos/scripts/test_e2e_env.sh)
|
||||||
|
cd e2e-tests
|
||||||
|
pnpm run ${{ matrix.test_command }} --reporter=html
|
||||||
|
|
||||||
|
- name: Upload Playwright report
|
||||||
|
if: ${{ failure() }}
|
||||||
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
|
with:
|
||||||
|
name: playwright-report-${{ matrix.boundary_edition }}
|
||||||
|
path: e2e-tests/playwright-report
|
||||||
|
|
||||||
|
- name: Clean up test infra
|
||||||
|
if: ${{ always() }}
|
||||||
|
run: |
|
||||||
|
export ENOS_VAR_boundary_edition=${{ matrix.boundary_edition_short }} && \
|
||||||
|
export ENOS_VAR_enos_user=$GITHUB_ACTOR && \
|
||||||
|
export ENOS_VAR_aws_ssh_private_key_path=~/.ssh/github_enos && \
|
||||||
|
enos scenario destroy --timeout 60m0s --chdir ./support/src/${{matrix.folder_name}}/enos ${{matrix.enos_scenario}} builder:local
|
||||||
|
|
||||||
|
- name: Send Slack message on failure
|
||||||
|
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
|
||||||
|
if: ${{ failure() && github.repository == 'hashicorp/boundary-ui' }}
|
||||||
|
with:
|
||||||
|
method: chat.postMessage
|
||||||
|
token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||||
|
payload: |
|
||||||
|
channel: ${{ secrets.SLACK_CHANNEL_ID }}
|
||||||
|
text: ":x: admin ui e2e tests failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\n*Branch:* ${{ github.repository }}:${{ github.head_ref }}"
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
name: Test E2E Desktop Client
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [labeled, opened, synchronize, reopened]
|
||||||
|
paths:
|
||||||
|
- 'ui/desktop/**'
|
||||||
|
- 'e2e-tests/*'
|
||||||
|
- 'e2e-tests/api-client/**'
|
||||||
|
- 'e2e-tests/helpers/**'
|
||||||
|
- 'e2e-tests/desktop/**'
|
||||||
|
- '.github/workflows/test-e2e-desktop.yaml'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: test
|
||||||
|
runs-on: ${{ fromJSON(vars.RUNNER_UBUNTU_22) }}
|
||||||
|
env:
|
||||||
|
ENOS_VAR_boundary_edition: "ent"
|
||||||
|
ENOS_VAR_e2e_debug_no_run: true
|
||||||
|
SETUP_CLI: true
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up pnpm
|
||||||
|
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 #v4.2.0
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Set up Node
|
||||||
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: "pnpm"
|
||||||
|
cache-dependency-path: |
|
||||||
|
pnpm-lock.yaml
|
||||||
|
ui/desktop/electron-app/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Set up Terraform
|
||||||
|
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd
|
||||||
|
|
||||||
|
- name: Set up Enos
|
||||||
|
uses: hashicorp/action-setup-enos@39a8f388434bea5eeba6649b99aa9949a55a1150
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Install Vault CLI
|
||||||
|
run: |
|
||||||
|
wget https://releases.hashicorp.com/vault/1.12.2/vault_1.12.2_linux_amd64.zip -O /tmp/vault.zip
|
||||||
|
unzip /tmp/vault.zip -d /usr/local/bin
|
||||||
|
|
||||||
|
- name: Checkout boundary-enterprise
|
||||||
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
with:
|
||||||
|
repository: hashicorp/boundary-enterprise
|
||||||
|
# ref: ${{ github.event.inputs.boundary-enterprise-branch }}
|
||||||
|
path: support/src/boundary-enterprise
|
||||||
|
token: ${{ secrets.ELEVATED_GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: GH fix for localhost resolution
|
||||||
|
run: |
|
||||||
|
cat /etc/hosts && echo "-----------"
|
||||||
|
sudo sed -i 's/::1 *localhost ip6-localhost ip6-loopback/::1 ip6 -localhost ip6-loopback/g' /etc/hosts
|
||||||
|
cat /etc/hosts
|
||||||
|
ssh -V
|
||||||
|
|
||||||
|
echo "127.0.0.1 localhost boundary" | sudo tee -a /etc/hosts
|
||||||
|
echo "127.0.0.1 localhost worker" | sudo tee -a /etc/hosts
|
||||||
|
echo "127.0.0.1 localhost vault" | sudo tee -a /etc/hosts
|
||||||
|
|
||||||
|
# TODO: Build image ourselves in case we don't want to use latest?
|
||||||
|
- name: Get Boundary docker image
|
||||||
|
run: |
|
||||||
|
docker pull hashicorp/boundary-enterprise:latest
|
||||||
|
docker save hashicorp/boundary-enterprise:latest | gzip > /tmp/boundary-enterprise.zip
|
||||||
|
|
||||||
|
- name: Set up test infra
|
||||||
|
id: infra
|
||||||
|
run: |
|
||||||
|
ssh-keygen -N '' -t ed25519 -f ~/.ssh/github_enos
|
||||||
|
mkdir -p ./enos/terraform-plugin-cache
|
||||||
|
export ENOS_VAR_enos_user=$GITHUB_ACTOR && \
|
||||||
|
export ENOS_VAR_aws_ssh_private_key_path=~/.ssh/github_enos && \
|
||||||
|
export ENOS_VAR_boundary_license="${{ secrets.BOUNDARY_ENT_LICENSE }}" && \
|
||||||
|
export ENOS_VAR_boundary_docker_image_file=/tmp/boundary-enterprise.zip && \
|
||||||
|
enos scenario launch --timeout 60m0s --chdir ./support/src/boundary-enterprise/enos e2e_ui_docker_ent builder:crt
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
cd e2e-tests
|
||||||
|
pnpm playwright install --with-deps
|
||||||
|
|
||||||
|
# Runner is headless so we need to enable a display to run the client
|
||||||
|
- name: Run Tests
|
||||||
|
run: |
|
||||||
|
source <(bash ./support/src/boundary-enterprise/enos/scripts/test_e2e_env.sh)
|
||||||
|
cd e2e-tests
|
||||||
|
export DISPLAY=:99
|
||||||
|
sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
|
||||||
|
pnpm run desktop
|
||||||
|
|
||||||
|
- name: Upload Playwright report
|
||||||
|
if: ${{ failure() }}
|
||||||
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: e2e-tests/playwright-report
|
||||||
|
|
||||||
|
- name: Clean up test infra
|
||||||
|
if: ${{ always() }}
|
||||||
|
run: |
|
||||||
|
export ENOS_VAR_enos_user=$GITHUB_ACTOR && \
|
||||||
|
export ENOS_VAR_aws_ssh_private_key_path=~/.ssh/github_enos && \
|
||||||
|
enos scenario destroy --timeout 60m0s --chdir ./support/src/boundary-enterprise/enos e2e_ui_docker_ent builder:crt
|
||||||
|
|
||||||
|
- name: Send Slack message on failure
|
||||||
|
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
|
||||||
|
if: ${{ failure() && github.repository == 'hashicorp/boundary-ui' }}
|
||||||
|
with:
|
||||||
|
method: chat.postMessage
|
||||||
|
token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||||
|
payload: |
|
||||||
|
channel: ${{ secrets.SLACK_CHANNEL_ID }}
|
||||||
|
text: ":x: desktop client e2e tests failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\n*Branch:* ${{ github.repository }}:${{ github.head_ref }}"
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
name: "Trigger Test Boundary UI release"
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
branch:
|
|
||||||
description: Please use tag-like format n.n.n-someword (1.111.11-beta)
|
|
||||||
required: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
trigger-gha-release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Release description
|
|
||||||
if: github.event_name == 'workflow_dispatch'
|
|
||||||
run: |
|
|
||||||
echo "Triggering Test release using BRANCH and TEST_TAG ${{ github.event.inputs.branch }}"
|
|
||||||
- name: Trigger Release workflow in boundary-ui-releases
|
|
||||||
run: |
|
|
||||||
export GITHUB_TOKEN="${{ secrets.ELEVATED_GITHUB_TOKEN }}"
|
|
||||||
gh workflow run release.yml \
|
|
||||||
--repo hashicorp/boundary-ui-releases \
|
|
||||||
--ref main \
|
|
||||||
-f TAG="${{ github.event.inputs.branch }}"
|
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: "Trigger Boundary UI release"
|
name: "Trigger Boundary Desktop release"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -7,16 +7,16 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
trigger-gha-release:
|
trigger-gha-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ fromJSON(vars.RUNNER) }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Release description
|
- name: Release description
|
||||||
run: |
|
run: |
|
||||||
echo "Triggering release with a TAG ${GITHUB_REF#refs/tags/}"
|
echo "Triggering release with a TAG ${GITHUB_REF#refs/tags/}"
|
||||||
- name: Trigger Release workflow in boundary-ui-releases
|
- name: Trigger Release workflow in boundary-desktop-releases
|
||||||
run: |
|
run: |
|
||||||
export GITHUB_TOKEN="${{ secrets.ELEVATED_GITHUB_TOKEN }}"
|
export GITHUB_TOKEN="${{ secrets.ELEVATED_GITHUB_TOKEN }}"
|
||||||
gh workflow run release.yml \
|
gh workflow run release.yml \
|
||||||
--repo hashicorp/boundary-ui-releases \
|
--repo hashicorp/boundary-desktop-releases \
|
||||||
--ref main \
|
--ref main \
|
||||||
-f TAG="${GITHUB_REF#refs/tags/}"
|
-f TAG="${GITHUB_REF#refs/tags/}"
|
||||||
@@ -19,7 +19,6 @@ coverage-*
|
|||||||
libpeerconnection.log
|
libpeerconnection.log
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
testem.log
|
testem.log
|
||||||
yarn-error.log
|
|
||||||
.watchman-cookie*
|
.watchman-cookie*
|
||||||
|
|
||||||
### VisualStudioCode ###
|
### VisualStudioCode ###
|
||||||
|
|||||||
+2
-5
@@ -1,5 +1,2 @@
|
|||||||
#!/usr/bin/env sh
|
pnpm compliance:licenses
|
||||||
. "$(dirname -- "$0")/_/husky.sh"
|
pnpm lint-staged
|
||||||
|
|
||||||
yarn compliance:licenses
|
|
||||||
npx lint-staged
|
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
. "$(dirname -- "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
COMMIT_MSG_FILE=$1
|
COMMIT_MSG_FILE=$1
|
||||||
COMMIT_SOURCE=$2
|
COMMIT_SOURCE=$2
|
||||||
SHA1=$3
|
SHA1=$3
|
||||||
@@ -24,4 +21,3 @@ else
|
|||||||
# This is to allow interaction with the terminal for commitizen
|
# This is to allow interaction with the terminal for commitizen
|
||||||
exec < /dev/tty && npx git-cz --hook || true
|
exec < /dev/tty && npx git-cz --hook || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ subproject, e.g. running or testing.
|
|||||||
- [Prerequisites](#prerequisites)
|
- [Prerequisites](#prerequisites)
|
||||||
- [Optional Tooling](#optional-tooling)
|
- [Optional Tooling](#optional-tooling)
|
||||||
- [Installation](#installation)
|
- [Installation](#installation)
|
||||||
- [Yarn Commands](#yarn-commands)
|
- [Pnpm Commands](#pnpm-commands)
|
||||||
- [Contributing](#contributing)
|
- [Contributing](#contributing)
|
||||||
- [Branching](#branching)
|
- [Branching](#branching)
|
||||||
- [Building ToC](#building-toc)
|
- [Building ToC](#building-toc)
|
||||||
@@ -33,8 +33,8 @@ subproject, e.g. running or testing.
|
|||||||
- [Building with a Container](#building-with-a-container)
|
- [Building with a Container](#building-with-a-container)
|
||||||
- [Building Desktop UI for Production](#building-desktop-ui-for-production)
|
- [Building Desktop UI for Production](#building-desktop-ui-for-production)
|
||||||
- [Connect Boundary UI to Boundary local instance](#connect-boundary-ui-to-boundary-local-instance)
|
- [Connect Boundary UI to Boundary local instance](#connect-boundary-ui-to-boundary-local-instance)
|
||||||
- [For admin](#for-admin)
|
- [For Admin](#for-admin)
|
||||||
- [For desktop](#for-desktop)
|
- [For Desktop](#for-desktop)
|
||||||
- [Committing](#committing)
|
- [Committing](#committing)
|
||||||
- [License Checking](#license-checking)
|
- [License Checking](#license-checking)
|
||||||
|
|
||||||
@@ -67,8 +67,8 @@ You will need the following things properly installed on your computer.
|
|||||||
|
|
||||||
* [Git][git]
|
* [Git][git]
|
||||||
* [Node.js][node]
|
* [Node.js][node]
|
||||||
* Supported versions: `v18` and `v20`.
|
* Supported versions: `v20` and `v22`.
|
||||||
* [Yarn][yarn] installed globally
|
* [Pnpm][pnpm] installed globally
|
||||||
* [Google Chrome][chrome]
|
* [Google Chrome][chrome]
|
||||||
* [Firefox][firefox]
|
* [Firefox][firefox]
|
||||||
|
|
||||||
@@ -90,18 +90,18 @@ that best suit you and your personal style.
|
|||||||
|
|
||||||
* `git clone <repository-url>` this repository
|
* `git clone <repository-url>` this repository
|
||||||
* `cd boundary-ui`
|
* `cd boundary-ui`
|
||||||
* `yarn install`
|
* `pnpm install`
|
||||||
|
|
||||||
## Yarn Commands
|
## Pnpm Commands
|
||||||
|
|
||||||
List of available project commands. `yarn run <command-name>`
|
List of available project commands. `pnpm run <command-name>`
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------------------|---|
|
|---------------------|---|
|
||||||
| build | Builds Admin UI for production. |
|
| build | Builds Admin UI for production. |
|
||||||
| test | Runs tests in CI mode. |
|
| test | Runs tests in CI mode. |
|
||||||
| lint | Runs ember-template-lint on all of the hbs, js, and sass files. |
|
| lint | Runs ember-template-lint on all of the hbs, js, and sass files. |
|
||||||
| compliance:audit | Checks for issues using `yarn audit` with moderate and above criticality.|
|
| compliance:audit | Checks for issues using `pnpm audit` with moderate and above criticality.|
|
||||||
| compliance:licenses | Checks that all dependencies have OSS-compatible licenses. |
|
| compliance:licenses | Checks that all dependencies have OSS-compatible licenses. |
|
||||||
| doc:toc | Automatically generates a table of contents for the README file. |
|
| doc:toc | Automatically generates a table of contents for the README file. |
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ We follow [GitHub Flow][github-flow].
|
|||||||
### Building ToC
|
### Building ToC
|
||||||
|
|
||||||
To autogenerate a ToC (table of contents) for this README,
|
To autogenerate a ToC (table of contents) for this README,
|
||||||
run `yarn doc:toc`. Please update the ToC whenever editing the structure
|
run `pnpm doc:toc`. Please update the ToC whenever editing the structure
|
||||||
of README.
|
of README.
|
||||||
|
|
||||||
### Building Admin UI for Production
|
### Building Admin UI for Production
|
||||||
@@ -126,8 +126,8 @@ necessary for your target environment, as described in the [Environment Variable
|
|||||||
To quickly produce a production build of Admin UI, run from this folder:
|
To quickly produce a production build of Admin UI, run from this folder:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn install
|
pnpm install
|
||||||
yarn build
|
pnpm build
|
||||||
```
|
```
|
||||||
|
|
||||||
Assets are saved to `ui/admin/dist/`.
|
Assets are saved to `ui/admin/dist/`.
|
||||||
@@ -151,20 +151,20 @@ necessary for your target environment, as described in the [Environment Variable
|
|||||||
To quickly produce a production build of Desktop UI, run from this folder:
|
To quickly produce a production build of Desktop UI, run from this folder:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn install
|
pnpm install
|
||||||
yarn build:ui:desktop
|
pnpm build:ui:desktop
|
||||||
```
|
```
|
||||||
|
|
||||||
In windows, UI is generated using docker to provide a stable UI across platforms.
|
In windows, UI is generated using docker to provide a stable UI across platforms.
|
||||||
|
|
||||||
```cmd
|
```cmd
|
||||||
yarn install
|
pnpm install
|
||||||
docker-compose -f docker-compose-embedding.yml run build-desktop-production
|
docker-compose -f docker-compose-embedding.yml run build-desktop-production
|
||||||
yarn build:ui:desktop:app
|
pnpm build:ui:desktop:app
|
||||||
```
|
```
|
||||||
|
|
||||||
To provide a signing identify for macOS build, use `BOUNDARY_DESKTOP_SIGNING_IDENTITY`
|
To provide a signing identify for macOS build, use `BOUNDARY_DESKTOP_SIGNING_IDENTITY`
|
||||||
environment variable to set signing certificate name (e.g Developer ID Application: * (*)) when building desktop app using `yarn build:ui:desktop:app`.
|
environment variable to set signing certificate name (e.g Developer ID Application: * (*)) when building desktop app using `pnpm build:ui:desktop:app`.
|
||||||
|
|
||||||
Assets are saved to `ui/desktop/electron-app/out/make/`.
|
Assets are saved to `ui/desktop/electron-app/out/make/`.
|
||||||
DMG packaged desktop UI is available at asset location as `boundary.dmg`.
|
DMG packaged desktop UI is available at asset location as `boundary.dmg`.
|
||||||
@@ -180,7 +180,7 @@ This assumes you already have up and running a Boundary dev instance in your loc
|
|||||||
|
|
||||||
This assumes you are within `boundary-ui/ui/admin`.
|
This assumes you are within `boundary-ui/ui/admin`.
|
||||||
|
|
||||||
You will need to turn `off` mirage, tell the UI where to find Boundary and run it: `$ ENABLE_MIRAGE=false API_HOST=http://localhost:9200 yarn start`.
|
You will need to turn `off` mirage, tell the UI where to find Boundary and run it: `$ ENABLE_MIRAGE=false API_HOST=http://localhost:9200 pnpm start`.
|
||||||
|
|
||||||
Following terminal instructions, open in your browser: `http://localhost:4200/`.
|
Following terminal instructions, open in your browser: `http://localhost:4200/`.
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ Once you open the UI you will see the login screen, authenticate using generated
|
|||||||
|
|
||||||
This assumes you are within `boundary-ui/ui/desktop`.
|
This assumes you are within `boundary-ui/ui/desktop`.
|
||||||
|
|
||||||
You will need to turn `off` mirage and run it: `$ ENABLE_MIRAGE=false yarn start:desktop`.
|
You will need to turn `off` mirage and run it: `$ ENABLE_MIRAGE=false pnpm start:desktop`.
|
||||||
|
|
||||||
In login screen, authenticate using generated admin login name and password.
|
In login screen, authenticate using generated admin login name and password.
|
||||||
|
|
||||||
@@ -199,7 +199,7 @@ Enter the cluster URL of your Boundary dev instance, by default is: `http://loca
|
|||||||
### Committing
|
### Committing
|
||||||
|
|
||||||
We use `husky` and `lint-staged` to ensure linting and other checks pass before a
|
We use `husky` and `lint-staged` to ensure linting and other checks pass before a
|
||||||
commit is completed. Simply do a `yarn install` to make sure the hooks are installed.
|
commit is completed. Simply do a `pnpm install` to make sure the hooks are installed.
|
||||||
|
|
||||||
Use a normal `git commit` to go through the checks, if you need to skip these checks,
|
Use a normal `git commit` to go through the checks, if you need to skip these checks,
|
||||||
use `git commit --no-verify` to bypass them. However, a consistent commit message will
|
use `git commit --no-verify` to bypass them. However, a consistent commit message will
|
||||||
@@ -218,10 +218,10 @@ will not be accepted.
|
|||||||
|
|
||||||
[git]: https://git-scm.com/
|
[git]: https://git-scm.com/
|
||||||
[node]: https://nodejs.org/
|
[node]: https://nodejs.org/
|
||||||
[yarn]: https://classic.yarnpkg.com/lang/en/
|
[pnpm]: https://pnpm.io
|
||||||
[chrome]: https://google.com/chrome/
|
[chrome]: https://google.com/chrome/
|
||||||
[firefox]: https://firefox.com/
|
[firefox]: https://firefox.com/
|
||||||
[yarn-workspaces]: https://classic.yarnpkg.com/en/docs/workspaces/
|
[pnpm-workspaces]: https://pnpm.io/workspaces
|
||||||
[homebrew]: https://brew.sh
|
[homebrew]: https://brew.sh
|
||||||
[warp]: https://www.warp.dev
|
[warp]: https://www.warp.dev
|
||||||
[vscode]: https://code.visualstudio.com/
|
[vscode]: https://code.visualstudio.com/
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ insert_final_newline = true
|
|||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
[*.{diff,md}]
|
|
||||||
trim_trailing_whitespace = false
|
|
||||||
|
|
||||||
[*.hbs]
|
[*.hbs]
|
||||||
insert_final_newline = false
|
insert_final_newline = false
|
||||||
|
|
||||||
|
[*.{diff,md}]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
@@ -1,12 +1,4 @@
|
|||||||
{
|
{
|
||||||
/**
|
|
||||||
Ember CLI sends analytics information by default. The data is completely
|
|
||||||
anonymous, but there are times when you might want to disable this behavior.
|
|
||||||
|
|
||||||
Setting `disableAnalytics` to true will prevent any data from being sent.
|
|
||||||
*/
|
|
||||||
"disableAnalytics": false,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript
|
Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript
|
||||||
rather than JavaScript by default, when a TypeScript version of a given blueprint is available.
|
rather than JavaScript by default, when a TypeScript version of a given blueprint is available.
|
||||||
|
|||||||
@@ -1,25 +1,14 @@
|
|||||||
# unconventional js
|
# unconventional js
|
||||||
/blueprints/*/files/
|
/blueprints/*/files/
|
||||||
/vendor/
|
|
||||||
|
|
||||||
# compiled output
|
# compiled output
|
||||||
|
/declarations/
|
||||||
/dist/
|
/dist/
|
||||||
/tmp/
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/bower_components/
|
|
||||||
/node_modules/
|
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
/coverage/
|
/coverage/
|
||||||
!.*
|
!.*
|
||||||
.*/
|
.*/
|
||||||
.eslintcache
|
|
||||||
|
|
||||||
# ember-try
|
# ember-try
|
||||||
/.node_modules.ember-try/
|
/.node_modules.ember-try/
|
||||||
/bower.json.ember-try
|
|
||||||
/npm-shrinkwrap.json.ember-try
|
|
||||||
/package.json.ember-try
|
|
||||||
/package-lock.json.ember-try
|
|
||||||
/yarn.lock.ember-try
|
|
||||||
@@ -26,6 +26,7 @@ module.exports = {
|
|||||||
browser: true,
|
browser: true,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
curly: ['error', 'multi-line', 'consistent'],
|
||||||
'ember/no-get': 'off',
|
'ember/no-get': 'off',
|
||||||
'ember/no-get-with-default': 'off',
|
'ember/no-get-with-default': 'off',
|
||||||
'ember/no-computed-properties-in-native-classes': 'off',
|
'ember/no-computed-properties-in-native-classes': 'off',
|
||||||
@@ -45,6 +46,7 @@ module.exports = {
|
|||||||
'./blueprints/*/index.js',
|
'./blueprints/*/index.js',
|
||||||
'./config/**/*.js',
|
'./config/**/*.js',
|
||||||
'./tests/dummy/config/**/*.js',
|
'./tests/dummy/config/**/*.js',
|
||||||
|
'./broccoli-plugins/**/*.js',
|
||||||
],
|
],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
sourceType: 'script',
|
sourceType: 'script',
|
||||||
@@ -56,7 +58,7 @@ module.exports = {
|
|||||||
extends: ['plugin:n/recommended'],
|
extends: ['plugin:n/recommended'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Test files:
|
// test files
|
||||||
files: ['tests/**/*-test.{js,ts}'],
|
files: ['tests/**/*-test.{js,ts}'],
|
||||||
extends: ['plugin:qunit/recommended'],
|
extends: ['plugin:qunit/recommended'],
|
||||||
rules: {
|
rules: {
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# compiled output
|
||||||
|
/dist/
|
||||||
|
/declarations/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules/
|
||||||
|
|
||||||
|
# misc
|
||||||
|
/.env*
|
||||||
|
/.pnp*
|
||||||
|
/.eslintcache
|
||||||
|
/coverage/
|
||||||
|
/testem.log
|
||||||
|
|
||||||
|
# ember-try
|
||||||
|
/.node_modules.ember-try/
|
||||||
|
/package.json.ember-try
|
||||||
|
|
||||||
|
# broccoli-debug
|
||||||
|
/DEBUG/
|
||||||
+2
-12
@@ -2,11 +2,7 @@
|
|||||||
/dist/
|
/dist/
|
||||||
/tmp/
|
/tmp/
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/bower_components/
|
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
/.bowerrc
|
|
||||||
/.editorconfig
|
/.editorconfig
|
||||||
/.ember-cli
|
/.ember-cli
|
||||||
/.env*
|
/.env*
|
||||||
@@ -19,21 +15,15 @@
|
|||||||
/.prettierignore
|
/.prettierignore
|
||||||
/.prettierrc.js
|
/.prettierrc.js
|
||||||
/.template-lintrc.js
|
/.template-lintrc.js
|
||||||
/.travis.yml
|
|
||||||
/.watchmanconfig
|
/.watchmanconfig
|
||||||
/bower.json
|
|
||||||
/CONTRIBUTING.md
|
/CONTRIBUTING.md
|
||||||
/ember-cli-build.js
|
/ember-cli-build.js
|
||||||
/testem.js
|
/testem.js
|
||||||
/tests/
|
/tests/
|
||||||
/yarn-error.log
|
/tsconfig.declarations.json
|
||||||
/yarn.lock
|
/tsconfig.json
|
||||||
.gitkeep
|
.gitkeep
|
||||||
|
|
||||||
# ember-try
|
# ember-try
|
||||||
/.node_modules.ember-try/
|
/.node_modules.ember-try/
|
||||||
/bower.json.ember-try
|
|
||||||
/npm-shrinkwrap.json.ember-try
|
|
||||||
/package.json.ember-try
|
/package.json.ember-try
|
||||||
/package-lock.json.ember-try
|
|
||||||
/yarn.lock.ember-try
|
|
||||||
@@ -1,25 +1,13 @@
|
|||||||
# unconventional js
|
# unconventional js
|
||||||
/blueprints/*/files/
|
/blueprints/*/files/
|
||||||
/vendor/
|
|
||||||
|
|
||||||
# compiled output
|
# compiled output
|
||||||
/dist/
|
/dist/
|
||||||
/tmp/
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/bower_components/
|
|
||||||
/node_modules/
|
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
/coverage/
|
/coverage/
|
||||||
!.*
|
!.*
|
||||||
.eslintcache
|
.*/
|
||||||
.lint-todo/
|
|
||||||
|
|
||||||
# ember-try
|
# ember-try
|
||||||
/.node_modules.ember-try/
|
/.node_modules.ember-try/
|
||||||
/bower.json.ember-try
|
|
||||||
/npm-shrinkwrap.json.ember-try
|
|
||||||
/package.json.ember-try
|
|
||||||
/package-lock.json.ember-try
|
|
||||||
/yarn.lock.ember-try
|
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
* SPDX-License-Identifier: BUSL-1.1
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"ignore_dirs": ["tmp", "dist"]
|
"ignore_dirs": ["dist"]
|
||||||
}
|
}
|
||||||
+15
-15
@@ -8,7 +8,7 @@ This addon contains the API data access layer for Boundary.
|
|||||||
|
|
||||||
- [Add API to an App](#add-api-to-an-app)
|
- [Add API to an App](#add-api-to-an-app)
|
||||||
- [Installation](#installation)
|
- [Installation](#installation)
|
||||||
- [Yarn Commands](#yarn-commands)
|
- [Pnpm Commands](#pnpm-commands)
|
||||||
- [Linting](#linting)
|
- [Linting](#linting)
|
||||||
- [Formatting](#formatting)
|
- [Formatting](#formatting)
|
||||||
- [Running tests](#running-tests)
|
- [Running tests](#running-tests)
|
||||||
@@ -21,7 +21,7 @@ This addon contains the API data access layer for Boundary.
|
|||||||
## Add API to an App
|
## Add API to an App
|
||||||
|
|
||||||
Add this addon to an Ember application's `devDependencies` as:
|
Add this addon to an Ember application's `devDependencies` as:
|
||||||
`"api": "*"`, for applications included in this monorepo.
|
`"api": "workspace:*"`, for applications included in this monorepo.
|
||||||
|
|
||||||
Since this addon also includes Mirage mocks, be sure to install
|
Since this addon also includes Mirage mocks, be sure to install
|
||||||
`ember-cli-mirage` and add the following config to your UI project:
|
`ember-cli-mirage` and add the following config to your UI project:
|
||||||
@@ -38,9 +38,9 @@ See monorepo README for installation instructions.
|
|||||||
|
|
||||||
When manually installing addon, ensure `ember-data-fragments@5.0.0-beta.*` is installed to use api models with data fragments.
|
When manually installing addon, ensure `ember-data-fragments@5.0.0-beta.*` is installed to use api models with data fragments.
|
||||||
|
|
||||||
## Yarn Commands
|
## Pnpm Commands
|
||||||
|
|
||||||
List of available project commands. `yarn run <command-name>`
|
List of available project commands. `pnpm run <command-name>`
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
| ------- | ----------- |
|
| ------- | ----------- |
|
||||||
@@ -64,28 +64,28 @@ Additional commands in the monorepo package may affect this projects.
|
|||||||
|
|
||||||
## Linting
|
## Linting
|
||||||
|
|
||||||
* `yarn lint:fix`
|
* `pnpm lint:fix`
|
||||||
* `yarn lint:hbs`
|
* `pnpm lint:hbs`
|
||||||
* `yarn lint:hbs:fix`
|
* `pnpm lint:hbs:fix`
|
||||||
* `yarn lint:js`
|
* `pnpm lint:js`
|
||||||
* `yarn lint:js:fix`
|
* `pnpm lint:js:fix`
|
||||||
|
|
||||||
## Formatting
|
## Formatting
|
||||||
|
|
||||||
Before submitting your work, be sure to run auto-formatters
|
Before submitting your work, be sure to run auto-formatters
|
||||||
(see commands above). This helps to ensure consistency among authors.
|
(see commands above). This helps to ensure consistency among authors.
|
||||||
|
|
||||||
* `yarn format`
|
* `pnpm format`
|
||||||
|
|
||||||
## Running tests
|
## Running tests
|
||||||
|
|
||||||
* `yarn test` – Runs the test suite on the current Ember version
|
* `pnpm test` – Runs the test suite on the current Ember version
|
||||||
* `yarn test --server` – Runs the test suite in "watch mode"
|
* `pnpm test --server` – Runs the test suite in "watch mode"
|
||||||
* `yarn test:all` – Runs the test suite against multiple Ember versions
|
* `pnpm test:all` – Runs the test suite against multiple Ember versions
|
||||||
|
|
||||||
## Running the dummy application
|
## Running the dummy application
|
||||||
|
|
||||||
* `yarn start`
|
* `pnpm start`
|
||||||
* Visit the dummy application at [http://localhost:4200](http://localhost:4200).
|
* Visit the dummy application at [http://localhost:4200](http://localhost:4200).
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
@@ -95,5 +95,5 @@ See monorepo README for more contribution instructions.
|
|||||||
### Building ToC
|
### Building ToC
|
||||||
|
|
||||||
To autogenerate a ToC (table of contents) for this README,
|
To autogenerate a ToC (table of contents) for this README,
|
||||||
run `yarn doc:toc`. Please update the ToC whenever editing the structure
|
run `pnpm doc:toc`. Please update the ToC whenever editing the structure
|
||||||
of README.
|
of README.
|
||||||
@@ -56,32 +56,33 @@ export default function setupStubs(hooks) {
|
|||||||
stubTypes = types;
|
stubTypes = types;
|
||||||
|
|
||||||
types.forEach((type, i) => {
|
types.forEach((type, i) => {
|
||||||
let models;
|
|
||||||
let resourceName;
|
|
||||||
if (typeOf(type) === 'object') {
|
|
||||||
models = type.func();
|
|
||||||
resourceName = type.resource;
|
|
||||||
} else {
|
|
||||||
models = this.server.schema[type].all().models;
|
|
||||||
resourceName = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ipcStub
|
this.ipcStub
|
||||||
.withArgs('searchCacheDaemon')
|
.withArgs('searchCacheDaemon')
|
||||||
.onCall(i)
|
.onCall(i)
|
||||||
.returns({
|
.callsFake(() => {
|
||||||
[underscore(resourceNames[singularize(resourceName)])]: models.map(
|
let models;
|
||||||
(model) => {
|
let resourceName;
|
||||||
// Use internal serializer to serialize the model correctly
|
if (typeOf(type) === 'object') {
|
||||||
// according to our mirage serializers
|
models = type.func();
|
||||||
const modelData =
|
resourceName = type.resource;
|
||||||
this.server.serializerOrRegistry.serialize(model);
|
} else {
|
||||||
|
models = this.server.schema[type].all().models;
|
||||||
|
resourceName = type;
|
||||||
|
}
|
||||||
|
|
||||||
// Serialize the data properly to standard JSON as that is what
|
return {
|
||||||
// we're expecting from the cache daemon response
|
[underscore(resourceNames[singularize(resourceName)])]:
|
||||||
return JSON.parse(JSON.stringify(modelData));
|
models.map((model) => {
|
||||||
},
|
// Use internal serializer to serialize the model correctly
|
||||||
),
|
// according to our mirage serializers
|
||||||
|
const modelData =
|
||||||
|
this.server.serializerOrRegistry.serialize(model);
|
||||||
|
|
||||||
|
// Serialize the data properly to standard JSON as that is what
|
||||||
|
// we're expecting from the cache daemon response
|
||||||
|
return JSON.parse(JSON.stringify(modelData));
|
||||||
|
}),
|
||||||
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) HashiCorp, Inc.
|
|
||||||
* SPDX-License-Identifier: BUSL-1.1
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This test helper can help set up and cleanup the database in your tests.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
|
|
||||||
*
|
|
||||||
* module('Acceptance | my test', function(hooks) {
|
|
||||||
* setupApplicationTest(hooks);
|
|
||||||
* setupIndexedDb(hooks);
|
|
||||||
*
|
|
||||||
* // add your actual tests here
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @param hooks
|
|
||||||
*/
|
|
||||||
export function setupIndexedDb(hooks) {
|
|
||||||
hooks.beforeEach(function () {
|
|
||||||
const indexedDb = this.owner.lookup('service:indexed-db');
|
|
||||||
indexedDb.setup(`test-indexed-db-${Date.now()}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
hooks.afterEach(async function () {
|
|
||||||
const indexedDb = this.owner.lookup('service:indexed-db');
|
|
||||||
|
|
||||||
await indexedDb.db.delete();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default setupIndexedDb;
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This test helper can help set up and cleanup the database in your tests.
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* import { setupSqlite } from 'api/test-support/helpers/sqlite';
|
||||||
|
*
|
||||||
|
* module('Acceptance | my test', function(hooks) {
|
||||||
|
* setupApplicationTest(hooks);
|
||||||
|
* setupSqlite(hooks);
|
||||||
|
*
|
||||||
|
* // add your actual tests here
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param hooks
|
||||||
|
*/
|
||||||
|
export function setupSqlite(hooks) {
|
||||||
|
hooks.beforeEach(async function () {
|
||||||
|
const sqliteDb = this.owner.lookup('service:sqlite');
|
||||||
|
await sqliteDb.setup(`test-sqlite-${uuidv4()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
hooks.afterEach(async function () {
|
||||||
|
const sqliteDb = this.owner.lookup('service:sqlite');
|
||||||
|
|
||||||
|
await sqliteDb.deleteDatabase();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default setupSqlite;
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import ModelAbility from './model';
|
import ModelAbility from './model';
|
||||||
import { inject as service } from '@ember/service';
|
import { service } from '@ember/service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides abilities for channel recordings.
|
* Provides abilities for channel recordings.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import ModelAbility from './model';
|
import ModelAbility from './model';
|
||||||
import { inject as service } from '@ember/service';
|
import { service } from '@ember/service';
|
||||||
/**
|
/**
|
||||||
* Provides abilities for credential store.
|
* Provides abilities for credential store.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import ModelAbility from './model';
|
import ModelAbility from './model';
|
||||||
import { inject as service } from '@ember/service';
|
import { service } from '@ember/service';
|
||||||
|
|
||||||
class InvalidRolePrincipalTypeError extends Error {
|
class InvalidRolePrincipalTypeError extends Error {
|
||||||
name = 'InvalidRolePrincipalTypeError';
|
name = 'InvalidRolePrincipalTypeError';
|
||||||
@@ -59,10 +59,11 @@ export default class RoleAbility extends ModelAbility {
|
|||||||
const type = this.model.constructor.modelName;
|
const type = this.model.constructor.modelName;
|
||||||
const typeIsValid =
|
const typeIsValid =
|
||||||
type === 'user' || type === 'group' || type === 'managed-group';
|
type === 'user' || type === 'group' || type === 'managed-group';
|
||||||
if (!typeIsValid)
|
if (!typeIsValid) {
|
||||||
throw new InvalidRolePrincipalTypeError(
|
throw new InvalidRolePrincipalTypeError(
|
||||||
`Expected a role principal of type 'user', 'group', or 'managed-group'. Got '${type}'.`,
|
`Expected a role principal of type 'user', 'group', or 'managed-group'. Got '${type}'.`,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
return this.can.can(`read ${type}`, this.model);
|
return this.can.can(`read ${type}`, this.model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,8 @@ export default class TargetAbility extends ModelAbility {
|
|||||||
*/
|
*/
|
||||||
get canAddInjectedApplicationCredentialSources() {
|
get canAddInjectedApplicationCredentialSources() {
|
||||||
return (
|
return (
|
||||||
this.model.isSSH && this.hasAuthorizedAction('add-credential-sources')
|
(this.model.isSSH || this.model.isRDP) &&
|
||||||
|
this.hasAuthorizedAction('add-credential-sources')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
|
|||||||
* @return {Promise} promise
|
* @return {Promise} promise
|
||||||
*/
|
*/
|
||||||
async query(store, schema, query) {
|
async query(store, schema, query) {
|
||||||
const queryObject = structuredClone(query);
|
const { batchLimit, ...queryObject } = query;
|
||||||
let result;
|
let result;
|
||||||
let data = [];
|
let data = [];
|
||||||
|
|
||||||
@@ -126,8 +126,10 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
|
|||||||
result = await super.query(store, schema, query);
|
result = await super.query(store, schema, query);
|
||||||
return prenormalizeArrayResponse(result);
|
return prenormalizeArrayResponse(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run this loop as long as the response_type is delta,
|
// Run this loop as long as the response_type is delta,
|
||||||
// which indicates that there are more items in the list
|
// which indicates that there are more items in the list
|
||||||
|
// or the batch size has not been reached
|
||||||
do {
|
do {
|
||||||
result = await super.query(store, schema, queryObject);
|
result = await super.query(store, schema, queryObject);
|
||||||
//add the result items to a data array
|
//add the result items to a data array
|
||||||
@@ -136,7 +138,10 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
|
|||||||
//pass in the list token for subsequent calls to fetch the remaining list items
|
//pass in the list token for subsequent calls to fetch the remaining list items
|
||||||
queryObject.list_token = result.list_token;
|
queryObject.list_token = result.list_token;
|
||||||
}
|
}
|
||||||
} while (result && result.response_type === 'delta');
|
} while (
|
||||||
|
result?.response_type === 'delta' &&
|
||||||
|
(!batchLimit || data.length < batchLimit)
|
||||||
|
);
|
||||||
|
|
||||||
result.items = data;
|
result.items = data;
|
||||||
return prenormalizeArrayResponse(result);
|
return prenormalizeArrayResponse(result);
|
||||||
@@ -163,7 +168,7 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Overrides default ajax method by rewritting PUT requests as PATCH.
|
* Overrides default ajax method by rewriting PUT requests as PATCH.
|
||||||
* PATCH is the request method used by our API for updates.
|
* PATCH is the request method used by our API for updates.
|
||||||
*
|
*
|
||||||
* @override
|
* @override
|
||||||
@@ -291,9 +296,7 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
|
|||||||
const errors = fieldErrors.map((error) => ({
|
const errors = fieldErrors.map((error) => ({
|
||||||
detail: error.description,
|
detail: error.description,
|
||||||
// Going forward, nested attributes will be hoisted. Thus "attributes."
|
// Going forward, nested attributes will be hoisted. Thus "attributes."
|
||||||
// must be stripped from the error name key. Until all models are
|
// must be stripped from the error name key.
|
||||||
// transitioned away from model fragments, however, both this and the
|
|
||||||
// prior error key mappings are necessary.
|
|
||||||
source: {
|
source: {
|
||||||
pointer: `/data/attributes/${error.name.replace('attributes.', '')}`,
|
pointer: `/data/attributes/${error.name.replace('attributes.', '')}`,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -81,9 +81,10 @@ export default class GeneratedCredentialLibraryModel extends BaseModel {
|
|||||||
})
|
})
|
||||||
credential_type;
|
credential_type;
|
||||||
|
|
||||||
@attr('object-as-array', {
|
@attr('object', {
|
||||||
for: 'vault-generic',
|
for: 'vault-generic',
|
||||||
description: 'It indicates the credential mapping overrides.',
|
description: 'It indicates the credential mapping overrides.',
|
||||||
|
emptyObjectIfMissing: true,
|
||||||
})
|
})
|
||||||
credential_mapping_overrides;
|
credential_mapping_overrides;
|
||||||
|
|
||||||
|
|||||||
@@ -51,17 +51,17 @@ export default class GeneratedCredentialModel extends BaseModel {
|
|||||||
})
|
})
|
||||||
version;
|
version;
|
||||||
|
|
||||||
// =attributes (username_password, ssh_private_key)
|
// =attributes (username_password, ssh_private_key, username_password_domain)
|
||||||
@attr('string', {
|
@attr('string', {
|
||||||
for: ['username_password', 'ssh_private_key'],
|
for: ['username_password', 'ssh_private_key', 'username_password_domain'],
|
||||||
isNestedAttribute: true,
|
isNestedAttribute: true,
|
||||||
description: 'The username for credential.',
|
description: 'The username for credential.',
|
||||||
})
|
})
|
||||||
username;
|
username;
|
||||||
|
|
||||||
// =attributes (username_password)
|
// =attributes (username_password, username_password_domain, password)
|
||||||
@attr('string', {
|
@attr('string', {
|
||||||
for: 'username_password',
|
for: ['username_password', 'username_password_domain', 'password'],
|
||||||
isNestedAttribute: true,
|
isNestedAttribute: true,
|
||||||
isSecret: true,
|
isSecret: true,
|
||||||
description: 'The password for credential.',
|
description: 'The password for credential.',
|
||||||
@@ -92,4 +92,12 @@ export default class GeneratedCredentialModel extends BaseModel {
|
|||||||
description: 'The secret associated with credential.',
|
description: 'The secret associated with credential.',
|
||||||
})
|
})
|
||||||
json_object;
|
json_object;
|
||||||
|
|
||||||
|
// =attributes (username_password_domain)
|
||||||
|
@attr('string', {
|
||||||
|
for: 'username_password_domain',
|
||||||
|
isNestedAttribute: true,
|
||||||
|
description: 'The domain for credential.',
|
||||||
|
})
|
||||||
|
domain;
|
||||||
}
|
}
|
||||||
@@ -55,23 +55,37 @@ export default class GeneratedHostCatalogModel extends BaseModel {
|
|||||||
})
|
})
|
||||||
plugin;
|
plugin;
|
||||||
|
|
||||||
// AWS & Azure
|
// AWS, Azure & GCP
|
||||||
|
|
||||||
@attr('boolean', {
|
@attr('boolean', {
|
||||||
|
for: {
|
||||||
|
type: 'plugin',
|
||||||
|
name: ['aws', 'azure', 'gcp'],
|
||||||
|
},
|
||||||
isNestedAttribute: true,
|
isNestedAttribute: true,
|
||||||
description: '',
|
description: '',
|
||||||
})
|
})
|
||||||
disable_credential_rotation;
|
disable_credential_rotation;
|
||||||
|
|
||||||
// AWS specific
|
// AWS and GCP specific
|
||||||
|
|
||||||
@attr('string', {
|
@attr('string', {
|
||||||
|
for: {
|
||||||
|
type: 'plugin',
|
||||||
|
name: ['aws', 'gcp'],
|
||||||
|
},
|
||||||
description:
|
description:
|
||||||
'An expression used to filter the workers that have network access to a service that is hosting the external object store.',
|
'An expression used to filter the workers that have network access to a service that is hosting the external object store.',
|
||||||
})
|
})
|
||||||
worker_filter;
|
worker_filter;
|
||||||
|
|
||||||
|
// AWS specific
|
||||||
|
|
||||||
@attr('string', {
|
@attr('string', {
|
||||||
|
for: {
|
||||||
|
type: 'plugin',
|
||||||
|
name: 'aws',
|
||||||
|
},
|
||||||
isNestedAttribute: true,
|
isNestedAttribute: true,
|
||||||
description: '',
|
description: '',
|
||||||
})
|
})
|
||||||
@@ -79,12 +93,22 @@ export default class GeneratedHostCatalogModel extends BaseModel {
|
|||||||
|
|
||||||
// AWS static credentials
|
// AWS static credentials
|
||||||
@attr('string', {
|
@attr('string', {
|
||||||
|
for: {
|
||||||
|
type: 'plugin',
|
||||||
|
name: 'aws',
|
||||||
|
credentialType: 'static-credential',
|
||||||
|
},
|
||||||
isNestedSecret: true,
|
isNestedSecret: true,
|
||||||
description: '',
|
description: '',
|
||||||
})
|
})
|
||||||
access_key_id;
|
access_key_id;
|
||||||
|
|
||||||
@attr('string', {
|
@attr('string', {
|
||||||
|
for: {
|
||||||
|
type: 'plugin',
|
||||||
|
name: 'aws',
|
||||||
|
credentialType: 'static-credential',
|
||||||
|
},
|
||||||
isNestedSecret: true,
|
isNestedSecret: true,
|
||||||
description: '',
|
description: '',
|
||||||
})
|
})
|
||||||
@@ -92,24 +116,44 @@ export default class GeneratedHostCatalogModel extends BaseModel {
|
|||||||
|
|
||||||
// AWS dynamic credentials
|
// AWS dynamic credentials
|
||||||
@attr('string', {
|
@attr('string', {
|
||||||
|
for: {
|
||||||
|
type: 'plugin',
|
||||||
|
name: 'aws',
|
||||||
|
credentialType: 'dynamic-credential',
|
||||||
|
},
|
||||||
isNestedAttribute: true,
|
isNestedAttribute: true,
|
||||||
description: 'The role ARN to use.',
|
description: 'The role ARN to use.',
|
||||||
})
|
})
|
||||||
role_arn;
|
role_arn;
|
||||||
|
|
||||||
@attr('string', {
|
@attr('string', {
|
||||||
|
for: {
|
||||||
|
type: 'plugin',
|
||||||
|
name: 'aws',
|
||||||
|
credentialType: 'dynamic-credential',
|
||||||
|
},
|
||||||
isNestedAttribute: true,
|
isNestedAttribute: true,
|
||||||
description: 'The role external ID to use.',
|
description: 'The role external ID to use.',
|
||||||
})
|
})
|
||||||
role_external_id;
|
role_external_id;
|
||||||
|
|
||||||
@attr('string', {
|
@attr('string', {
|
||||||
|
for: {
|
||||||
|
type: 'plugin',
|
||||||
|
name: 'aws',
|
||||||
|
credentialType: 'dynamic-credential',
|
||||||
|
},
|
||||||
isNestedAttribute: true,
|
isNestedAttribute: true,
|
||||||
description: 'The role session to use.',
|
description: 'The role session to use.',
|
||||||
})
|
})
|
||||||
role_session_name;
|
role_session_name;
|
||||||
|
|
||||||
@attr('object-as-array', {
|
@attr('object-as-array', {
|
||||||
|
for: {
|
||||||
|
type: 'plugin',
|
||||||
|
name: 'aws',
|
||||||
|
credentialType: 'dynamic-credential',
|
||||||
|
},
|
||||||
isNestedAttribute: true,
|
isNestedAttribute: true,
|
||||||
description: 'The role tags to use.',
|
description: 'The role tags to use.',
|
||||||
})
|
})
|
||||||
@@ -118,32 +162,117 @@ export default class GeneratedHostCatalogModel extends BaseModel {
|
|||||||
// Azure specific
|
// Azure specific
|
||||||
|
|
||||||
@attr('string', {
|
@attr('string', {
|
||||||
|
for: {
|
||||||
|
type: 'plugin',
|
||||||
|
name: 'azure',
|
||||||
|
},
|
||||||
isNestedAttribute: true,
|
isNestedAttribute: true,
|
||||||
description: '',
|
description: '',
|
||||||
})
|
})
|
||||||
tenant_id;
|
tenant_id;
|
||||||
|
|
||||||
@attr('string', {
|
@attr('string', {
|
||||||
|
for: {
|
||||||
|
type: 'plugin',
|
||||||
|
name: 'azure',
|
||||||
|
},
|
||||||
isNestedAttribute: true,
|
isNestedAttribute: true,
|
||||||
description: '',
|
description: '',
|
||||||
})
|
})
|
||||||
client_id;
|
client_id;
|
||||||
|
|
||||||
@attr('string', {
|
@attr('string', {
|
||||||
|
for: {
|
||||||
|
type: 'plugin',
|
||||||
|
name: 'azure',
|
||||||
|
},
|
||||||
isNestedAttribute: true,
|
isNestedAttribute: true,
|
||||||
description: '',
|
description: '',
|
||||||
})
|
})
|
||||||
subscription_id;
|
subscription_id;
|
||||||
|
|
||||||
@attr('string', {
|
@attr('string', {
|
||||||
|
for: {
|
||||||
|
type: 'plugin',
|
||||||
|
name: 'azure',
|
||||||
|
},
|
||||||
isNestedSecret: true,
|
isNestedSecret: true,
|
||||||
description: '',
|
description: '',
|
||||||
})
|
})
|
||||||
secret_id;
|
secret_id;
|
||||||
|
|
||||||
@attr('string', {
|
@attr('string', {
|
||||||
|
for: {
|
||||||
|
type: 'plugin',
|
||||||
|
name: 'azure',
|
||||||
|
},
|
||||||
isNestedSecret: true,
|
isNestedSecret: true,
|
||||||
description: '',
|
description: '',
|
||||||
})
|
})
|
||||||
secret_value;
|
secret_value;
|
||||||
|
|
||||||
|
// GCP specific
|
||||||
|
@attr('string', {
|
||||||
|
for: {
|
||||||
|
type: 'plugin',
|
||||||
|
name: 'gcp',
|
||||||
|
},
|
||||||
|
isNestedAttribute: true,
|
||||||
|
description: 'The project ID associated with the service account.',
|
||||||
|
})
|
||||||
|
project_id;
|
||||||
|
|
||||||
|
@attr('string', {
|
||||||
|
for: {
|
||||||
|
type: 'plugin',
|
||||||
|
name: 'gcp',
|
||||||
|
},
|
||||||
|
isNestedAttribute: true,
|
||||||
|
description: 'The deployment area within a region.',
|
||||||
|
})
|
||||||
|
zone;
|
||||||
|
|
||||||
|
@attr('string', {
|
||||||
|
for: {
|
||||||
|
type: 'plugin',
|
||||||
|
name: 'gcp',
|
||||||
|
},
|
||||||
|
isNestedAttribute: true,
|
||||||
|
description:
|
||||||
|
'The email address associated with the service account. The email address used to uniquely identify the service account. It is required for authentication and authorization.',
|
||||||
|
})
|
||||||
|
client_email;
|
||||||
|
|
||||||
|
@attr('string', {
|
||||||
|
for: {
|
||||||
|
type: 'plugin',
|
||||||
|
name: 'gcp',
|
||||||
|
},
|
||||||
|
isNestedAttribute: true,
|
||||||
|
description:
|
||||||
|
'The unique identifier for the service account that will be impersonated.',
|
||||||
|
})
|
||||||
|
target_service_account_id;
|
||||||
|
|
||||||
|
@attr('string', {
|
||||||
|
for: {
|
||||||
|
type: 'plugin',
|
||||||
|
name: 'gcp',
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
'The ID of the private key used to sign the JWT and authentication.',
|
||||||
|
isNestedSecret: true,
|
||||||
|
})
|
||||||
|
private_key_id;
|
||||||
|
|
||||||
|
@attr('string', {
|
||||||
|
for: {
|
||||||
|
type: 'plugin',
|
||||||
|
name: 'gcp',
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
'The private key used to sign the JWT and obtain a OAuth 2.0 access token.',
|
||||||
|
isNestedSecret: true,
|
||||||
|
})
|
||||||
|
private_key;
|
||||||
}
|
}
|
||||||
@@ -63,6 +63,10 @@ export default class GeneratedHostSetModel extends BaseModel {
|
|||||||
plugin;
|
plugin;
|
||||||
|
|
||||||
@attr('number', {
|
@attr('number', {
|
||||||
|
for: {
|
||||||
|
type: 'plugin',
|
||||||
|
name: ['aws', 'azure', 'gcp'],
|
||||||
|
},
|
||||||
description:
|
description:
|
||||||
"The number of seconds between the time boundary syncs the hosts in this set using this host set's plugin. If not provided a system determined default is used",
|
"The number of seconds between the time boundary syncs the hosts in this set using this host set's plugin. If not provided a system determined default is used",
|
||||||
})
|
})
|
||||||
@@ -79,12 +83,21 @@ export default class GeneratedHostSetModel extends BaseModel {
|
|||||||
host_ids;
|
host_ids;
|
||||||
|
|
||||||
@attr('string-array', {
|
@attr('string-array', {
|
||||||
|
for: {
|
||||||
|
type: 'plugin',
|
||||||
|
name: ['aws', 'azure', 'gcp'],
|
||||||
|
},
|
||||||
emptyArrayIfMissing: true,
|
emptyArrayIfMissing: true,
|
||||||
|
trimWhitespace: true,
|
||||||
})
|
})
|
||||||
preferred_endpoints;
|
preferred_endpoints;
|
||||||
|
|
||||||
// AWS specific
|
// AWS & GCP specific
|
||||||
@attr('string-array', {
|
@attr('string-array', {
|
||||||
|
for: {
|
||||||
|
type: 'plugin',
|
||||||
|
name: ['aws', 'gcp'],
|
||||||
|
},
|
||||||
isNestedAttribute: true,
|
isNestedAttribute: true,
|
||||||
emptyArrayIfMissing: true,
|
emptyArrayIfMissing: true,
|
||||||
})
|
})
|
||||||
@@ -92,6 +105,10 @@ export default class GeneratedHostSetModel extends BaseModel {
|
|||||||
|
|
||||||
// Azure specific, comes in from API as "filter". This is to avoid collisions with filter
|
// Azure specific, comes in from API as "filter". This is to avoid collisions with filter
|
||||||
@attr('string', {
|
@attr('string', {
|
||||||
|
for: {
|
||||||
|
type: 'plugin',
|
||||||
|
name: 'azure',
|
||||||
|
},
|
||||||
description: '',
|
description: '',
|
||||||
isNestedAttribute: true,
|
isNestedAttribute: true,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export default class GeneratedStorageBucketModel extends BaseModel {
|
|||||||
@attr('string', {
|
@attr('string', {
|
||||||
isNestedAttribute: true,
|
isNestedAttribute: true,
|
||||||
description: 'The AWS region to use.',
|
description: 'The AWS region to use.',
|
||||||
|
trimWhitespace: true,
|
||||||
})
|
})
|
||||||
region;
|
region;
|
||||||
|
|
||||||
@@ -109,6 +110,7 @@ export default class GeneratedStorageBucketModel extends BaseModel {
|
|||||||
@attr('string', {
|
@attr('string', {
|
||||||
isNestedAttribute: true,
|
isNestedAttribute: true,
|
||||||
description: 'The role ARN to use.',
|
description: 'The role ARN to use.',
|
||||||
|
trimWhitespace: true,
|
||||||
})
|
})
|
||||||
role_arn;
|
role_arn;
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,14 @@
|
|||||||
* SPDX-License-Identifier: BUSL-1.1
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* global __electronLog */
|
import { service } from '@ember/service';
|
||||||
import { inject as service } from '@ember/service';
|
|
||||||
import { getOwner, setOwner } from '@ember/application';
|
import { getOwner, setOwner } from '@ember/application';
|
||||||
import { generateMQLExpression } from '../utils/mql-query';
|
import { generateMQLExpression } from '../utils/mql-query';
|
||||||
import { paginateResults } from '../utils/paginate-results';
|
import { paginateResults } from '../utils/paginate-results';
|
||||||
import { underscore } from '@ember/string';
|
import { underscore } from '@ember/string';
|
||||||
|
|
||||||
|
const { __electronLog } = globalThis;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Not all types are yet supported by the cache daemon so we'll
|
* Not all types are yet supported by the cache daemon so we'll
|
||||||
* just whitelist the ones we need to start with. Keys are the resource
|
* just whitelist the ones we need to start with. Keys are the resource
|
||||||
@@ -42,7 +43,10 @@ const fetchControllerData = async (context, next) => {
|
|||||||
// If we get an error or cache daemon is unavailable, fall back to calling the API
|
// If we get an error or cache daemon is unavailable, fall back to calling the API
|
||||||
context.request.data.query = remainingQuery;
|
context.request.data.query = remainingQuery;
|
||||||
const results = await next(context.request);
|
const results = await next(context.request);
|
||||||
const models = results.content.toArray();
|
|
||||||
|
// As a result of https://rfcs.emberjs.com/id/0846-ember-data-deprecate-proxies/ we cannot use proxy array methods like .toArray
|
||||||
|
// use slice() instead as indicated in the RFC.
|
||||||
|
const models = results.content.slice();
|
||||||
|
|
||||||
const paginatedResult = paginateResults(models, page, pageSize);
|
const paginatedResult = paginateResults(models, page, pageSize);
|
||||||
paginatedResult.meta = { totalItems: models.length };
|
paginatedResult.meta = { totalItems: models.length };
|
||||||
|
|||||||
@@ -1,172 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) HashiCorp, Inc.
|
|
||||||
* SPDX-License-Identifier: BUSL-1.1
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { modelIndexes } from 'api/services/indexed-db';
|
|
||||||
import { inject as service } from '@ember/service';
|
|
||||||
import { getOwner, setOwner } from '@ember/application';
|
|
||||||
import { queryIndexedDb } from '../utils/indexed-db-query';
|
|
||||||
import { paginateResults } from '../utils/paginate-results';
|
|
||||||
import { hashCode } from '../utils/hash-code';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler to sit in front of the API request layer
|
|
||||||
* so we can handle any caching with indexedDb first
|
|
||||||
*/
|
|
||||||
export default class IndexedDbHandler {
|
|
||||||
@service indexedDb;
|
|
||||||
|
|
||||||
constructor(context) {
|
|
||||||
setOwner(this, getOwner(context));
|
|
||||||
}
|
|
||||||
|
|
||||||
async request(context, next) {
|
|
||||||
switch (context.request.op) {
|
|
||||||
case 'query': {
|
|
||||||
const { store, data } = context.request;
|
|
||||||
// TODO: Remove storeToken option as this is a temporary fix for auth-methods.
|
|
||||||
const {
|
|
||||||
type,
|
|
||||||
query,
|
|
||||||
options: {
|
|
||||||
pushToStore = true,
|
|
||||||
peekIndexedDB = false,
|
|
||||||
storeToken = true,
|
|
||||||
} = {},
|
|
||||||
} = data;
|
|
||||||
const supportedModels = Object.keys(modelIndexes);
|
|
||||||
const { db: indexedDb } = this.indexedDb ?? {};
|
|
||||||
|
|
||||||
// Go through normal flow if we don't yet support the model
|
|
||||||
// or if we don't have an indexedDb instance
|
|
||||||
if (!supportedModels.includes(type) || !indexedDb) {
|
|
||||||
return next(context.request);
|
|
||||||
}
|
|
||||||
|
|
||||||
let { page, pageSize, query: queryObj, ...remainingQuery } = query;
|
|
||||||
|
|
||||||
const adapter = store.adapterFor(type);
|
|
||||||
const schema = store.modelFor(type);
|
|
||||||
const serializer = store.serializerFor(type);
|
|
||||||
|
|
||||||
if (!peekIndexedDB) {
|
|
||||||
const tokenKey = `${type}-${hashCode(remainingQuery)}`;
|
|
||||||
|
|
||||||
let listToken;
|
|
||||||
if (storeToken) {
|
|
||||||
listToken = await indexedDb.token.get(tokenKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
let payload;
|
|
||||||
try {
|
|
||||||
payload = await adapter.query(store, schema, {
|
|
||||||
...remainingQuery,
|
|
||||||
list_token: listToken?.token,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
// If we get an invalid list token, we'll delete the token and
|
|
||||||
// clear the DB and try again without a token.
|
|
||||||
if (
|
|
||||||
e?.errors[0].status === 400 &&
|
|
||||||
e?.errors[0].code === 'invalid list token'
|
|
||||||
) {
|
|
||||||
await indexedDb[type].clear();
|
|
||||||
// Delete all tokens of the same resource type so we don't keep clearing the DB.
|
|
||||||
// Even if other tokens may still be valid, we might have cleared data that they
|
|
||||||
// depended on so we should clear all tokens of the same type when clearing the DB.
|
|
||||||
const tokenKeys = await indexedDb.token
|
|
||||||
.where(':id')
|
|
||||||
.startsWith(type)
|
|
||||||
.primaryKeys();
|
|
||||||
|
|
||||||
await indexedDb.token.bulkDelete(tokenKeys);
|
|
||||||
|
|
||||||
payload = await adapter.query(store, schema, {
|
|
||||||
...remainingQuery,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the token we just got back from the payload if it exists
|
|
||||||
if (payload.list_token && storeToken) {
|
|
||||||
await indexedDb.token.put({
|
|
||||||
id: tokenKey,
|
|
||||||
token: payload.list_token,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove any records from the DB if the API indicates they've been deleted
|
|
||||||
if (payload.removed_ids?.length > 0) {
|
|
||||||
await indexedDb[type].bulkDelete(payload.removed_ids);
|
|
||||||
}
|
|
||||||
// This is a temporary fix of clearing the DB (specifically for auth-methods)
|
|
||||||
// since we are not storing the token we do not get back a list of removed_ids
|
|
||||||
// from the API call and we do not want deleted items from showing in list view.
|
|
||||||
if (!storeToken) {
|
|
||||||
await indexedDb[type].clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedPayload = serializer.normalizeResponse(
|
|
||||||
store,
|
|
||||||
schema,
|
|
||||||
payload,
|
|
||||||
null,
|
|
||||||
'query',
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: payloadData } = normalizedPayload;
|
|
||||||
|
|
||||||
// Store the new data we just got back from the API refresh
|
|
||||||
const items = payloadData.map((datum) =>
|
|
||||||
this.indexedDb.normalizeData({
|
|
||||||
data: datum,
|
|
||||||
cleanData: true,
|
|
||||||
schema,
|
|
||||||
serializer,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await indexedDb[type].bulkPut(items);
|
|
||||||
}
|
|
||||||
|
|
||||||
const indexedDbResults = await queryIndexedDb(
|
|
||||||
indexedDb,
|
|
||||||
type,
|
|
||||||
queryObj,
|
|
||||||
);
|
|
||||||
|
|
||||||
const dbRecords = paginateResults(indexedDbResults, page, pageSize).map(
|
|
||||||
(item) =>
|
|
||||||
this.indexedDb.normalizeData({
|
|
||||||
data: item,
|
|
||||||
cleanData: false,
|
|
||||||
schema,
|
|
||||||
serializer,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Return the raw data if we don't push to the store.
|
|
||||||
let records = dbRecords.map((record) => ({
|
|
||||||
...record.attributes,
|
|
||||||
id: record.id,
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (pushToStore) {
|
|
||||||
records = store.push({ data: dbRecords });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set a meta property on the array to store the total items of the results.
|
|
||||||
// This isn't conventional but is better than returning an ArrayProxy
|
|
||||||
// or EmberArray since the ember store query method asserts it has to be an array
|
|
||||||
// so we can't just return an object.
|
|
||||||
records.meta = { totalItems: indexedDbResults.length };
|
|
||||||
return records;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return next(context.request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getOwner, setOwner } from '@ember/owner';
|
||||||
|
import { service } from '@ember/service';
|
||||||
|
import { modelMapping } from 'api/services/sqlite';
|
||||||
|
import { get } from '@ember/object';
|
||||||
|
import { typeOf } from '@ember/utils';
|
||||||
|
import { hashCode } from '../utils/hash-code';
|
||||||
|
import { generateSQLExpressions } from '../utils/sqlite-query';
|
||||||
|
|
||||||
|
const isISODateString = (str) =>
|
||||||
|
typeOf(str) === 'string' &&
|
||||||
|
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z?$/.test(str);
|
||||||
|
|
||||||
|
export default class SqliteHandler {
|
||||||
|
@service sqlite;
|
||||||
|
|
||||||
|
batchLimit = 5_000;
|
||||||
|
|
||||||
|
constructor(context) {
|
||||||
|
setOwner(this, getOwner(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
async request(context, next) {
|
||||||
|
switch (context.request.op) {
|
||||||
|
case 'query': {
|
||||||
|
const { store, data } = context.request;
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
query,
|
||||||
|
options: {
|
||||||
|
pushToStore = true,
|
||||||
|
peekDb = false,
|
||||||
|
storeToken = true,
|
||||||
|
} = {},
|
||||||
|
} = data;
|
||||||
|
const supportedModels = Object.keys(modelMapping);
|
||||||
|
|
||||||
|
// Go through normal flow if we don't yet support the model
|
||||||
|
// or if we don't have a sqlite db instance
|
||||||
|
if (!supportedModels.includes(type) || !this.sqlite.worker) {
|
||||||
|
return next(context.request);
|
||||||
|
}
|
||||||
|
|
||||||
|
const adapter = store.adapterFor(type);
|
||||||
|
const schema = store.modelFor(type);
|
||||||
|
const serializer = store.serializerFor(type);
|
||||||
|
|
||||||
|
let {
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
select,
|
||||||
|
query: queryObj,
|
||||||
|
...remainingQuery
|
||||||
|
} = query;
|
||||||
|
let payload,
|
||||||
|
listToken,
|
||||||
|
writeToDbPromise,
|
||||||
|
totalInsert = 0;
|
||||||
|
|
||||||
|
if (!peekDb) {
|
||||||
|
const tokenKey = `${type}-${hashCode(remainingQuery)}`;
|
||||||
|
if (storeToken) {
|
||||||
|
const [tokenObj] = await this.sqlite.fetchResource({
|
||||||
|
...generateSQLExpressions('token', {
|
||||||
|
filters: { id: [{ equals: tokenKey }] },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
listToken = tokenObj?.token;
|
||||||
|
} else {
|
||||||
|
// This is a temporary fix of clearing the DB (specifically for auth-methods)
|
||||||
|
// since we are not storing the token we do not get back a list of removed_ids
|
||||||
|
// from the API call and we do not want deleted items from showing in list view.
|
||||||
|
await this.sqlite.deleteResource(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try {
|
||||||
|
payload = await adapter.query(store, schema, {
|
||||||
|
...remainingQuery,
|
||||||
|
list_token: listToken,
|
||||||
|
batchLimit: this.batchLimit,
|
||||||
|
});
|
||||||
|
totalInsert += payload.items?.length ?? 0;
|
||||||
|
|
||||||
|
// await the previous writeToDbPromise before writing to db again
|
||||||
|
if (writeToDbPromise) {
|
||||||
|
await writeToDbPromise;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
payload = await this.retryQueryFailure({
|
||||||
|
err,
|
||||||
|
type,
|
||||||
|
adapter,
|
||||||
|
store,
|
||||||
|
schema,
|
||||||
|
remainingQuery,
|
||||||
|
});
|
||||||
|
totalInsert += payload.items?.length ?? 0;
|
||||||
|
}
|
||||||
|
listToken = payload.list_token;
|
||||||
|
|
||||||
|
writeToDbPromise = this.writeToDb(
|
||||||
|
payload,
|
||||||
|
storeToken,
|
||||||
|
tokenKey,
|
||||||
|
pushToStore,
|
||||||
|
serializer,
|
||||||
|
store,
|
||||||
|
schema,
|
||||||
|
type,
|
||||||
|
);
|
||||||
|
} while (payload.response_type === 'delta');
|
||||||
|
|
||||||
|
await writeToDbPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalInsert > 0) {
|
||||||
|
// If there were any inserts, let sqlite handle running analyze on the DB
|
||||||
|
await this.sqlite.analyzeDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sql, parameters } = generateSQLExpressions(type, queryObj, {
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
select: select ?? [{ field: 'data' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const rows = await this.sqlite.fetchResource({
|
||||||
|
sql,
|
||||||
|
parameters,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { sql: countSql, parameters: countParams } =
|
||||||
|
generateSQLExpressions(type, queryObj, {
|
||||||
|
select: [{ field: '*', isCount: true, alias: 'total' }],
|
||||||
|
});
|
||||||
|
const count = await this.sqlite.fetchResource({
|
||||||
|
sql: countSql,
|
||||||
|
parameters: countParams,
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = rows.map((item) =>
|
||||||
|
JSON.parse(item.data, (key, value) =>
|
||||||
|
// Reviver function to convert any ISO strings back to date objects
|
||||||
|
// as we had to initially convert them to strings when stored in SQLite
|
||||||
|
isISODateString(value) ? new Date(value) : value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// If we are not pushing to the store, use the raw data with id property
|
||||||
|
const records = pushToStore
|
||||||
|
? store.push({ data: results })
|
||||||
|
: results.map((record) => ({
|
||||||
|
...record.attributes,
|
||||||
|
id: record.id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Set a meta property on the array to store the total items of the results.
|
||||||
|
// This isn't conventional but is better than returning an ArrayProxy
|
||||||
|
// or EmberArray since the ember store query method asserts it has to be an array
|
||||||
|
// so we can't just return an object.
|
||||||
|
records.meta = { totalItems: count[0].total };
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return next(context.request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async retryQueryFailure({
|
||||||
|
err,
|
||||||
|
type,
|
||||||
|
adapter,
|
||||||
|
store,
|
||||||
|
schema,
|
||||||
|
remainingQuery,
|
||||||
|
}) {
|
||||||
|
// If we get an invalid list token, we'll delete the token and
|
||||||
|
// clear the DB and try again without a token.
|
||||||
|
if (
|
||||||
|
err?.errors?.[0].status === 400 &&
|
||||||
|
err?.errors?.[0].code === 'invalid list token'
|
||||||
|
) {
|
||||||
|
await this.sqlite.deleteResource(type);
|
||||||
|
// Delete all tokens of the same resource type so we don't keep clearing the DB.
|
||||||
|
// Even if other tokens may still be valid, we might have cleared data that they
|
||||||
|
// depended on so we should clear all tokens of the same type when clearing the DB.
|
||||||
|
const { sql, parameters } = generateSQLExpressions('token', {
|
||||||
|
filters: { id: [{ contains: type }] },
|
||||||
|
});
|
||||||
|
const tokenRows = await this.sqlite.fetchResource({
|
||||||
|
sql,
|
||||||
|
parameters,
|
||||||
|
});
|
||||||
|
const tokenIds = tokenRows.map((row) => row.id);
|
||||||
|
|
||||||
|
if (tokenIds.length > 0) {
|
||||||
|
await this.sqlite.deleteResource('token', tokenIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await adapter.query(store, schema, {
|
||||||
|
...remainingQuery,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeToDb(
|
||||||
|
payload,
|
||||||
|
storeToken,
|
||||||
|
tokenKey,
|
||||||
|
pushToStore,
|
||||||
|
serializer,
|
||||||
|
store,
|
||||||
|
schema,
|
||||||
|
type,
|
||||||
|
) {
|
||||||
|
if (payload.list_token && storeToken) {
|
||||||
|
await this.sqlite.insertResource('token', [
|
||||||
|
[tokenKey, payload.list_token],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedPayload = serializer.normalizeResponse(
|
||||||
|
store,
|
||||||
|
schema,
|
||||||
|
payload,
|
||||||
|
null,
|
||||||
|
'query',
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: payloadData } = normalizedPayload;
|
||||||
|
|
||||||
|
// Remove any records from the DB if the API indicates they've been deleted
|
||||||
|
// Additionally remove deleted records from ember data store for symmetry
|
||||||
|
if (payload.removed_ids?.length > 0) {
|
||||||
|
await this.sqlite.deleteResource(type, payload.removed_ids);
|
||||||
|
|
||||||
|
if (pushToStore) {
|
||||||
|
payload.removed_ids.forEach((id) => {
|
||||||
|
const record = store.peekRecord(type, id);
|
||||||
|
store.unloadRecord(record);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the new data we just got back from the API refresh
|
||||||
|
const items = payloadData.map((datum) => {
|
||||||
|
const params = Object.values(modelMapping[type]).map((key) => {
|
||||||
|
let value;
|
||||||
|
|
||||||
|
if (key === 'id') {
|
||||||
|
value = datum.id;
|
||||||
|
} else {
|
||||||
|
value = get(datum, `attributes.${key}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeOf(value) === 'date') {
|
||||||
|
return value.toISOString();
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...params, JSON.stringify(datum)];
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.sqlite.insertResource(type, items);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
import Mixin from '@ember/object/mixin';
|
import Mixin from '@ember/object/mixin';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Overrides URL-building methods in an adpater for greater flexibility.
|
* Overrides URL-building methods in an adapter for greater flexibility.
|
||||||
* Specifically enhances calls to `urlPrefix` with `modelName`, `id`, and
|
* Specifically enhances calls to `urlPrefix` with `modelName`, `id`, and
|
||||||
* `snapshot`, which aren't normally passed. And adds support for a
|
* `snapshot`, which aren't normally passed. And adds support for a
|
||||||
* `urlSuffix` method, the return value of which is appended to the end of
|
* `urlSuffix` method, the return value of which is appended to the end of
|
||||||
@@ -22,7 +22,7 @@ export default Mixin.create({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This override behaves identically to the default method by default.
|
* This override behaves identically to the default method by default.
|
||||||
* It exists only to document the additional paramters to the method.
|
* It exists only to document the additional parameters to the method.
|
||||||
* @override
|
* @override
|
||||||
* @param {string} path
|
* @param {string} path
|
||||||
* @param {string} parentURL
|
* @param {string} parentURL
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ export default class AccountModel extends GeneratedAccountModel {
|
|||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
get accountName() {
|
get accountName() {
|
||||||
const { email, full_name, login_name } = this;
|
const { email, full_name, login_name, subject } = this;
|
||||||
return email || full_name || login_name;
|
return email || full_name || login_name || subject;
|
||||||
}
|
}
|
||||||
|
|
||||||
// =methods
|
// =methods
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import Model from '@ember-data/model';
|
import Model from '@ember-data/model';
|
||||||
import { attr } from '@ember-data/model';
|
import { attr } from '@ember-data/model';
|
||||||
import { computed } from '@ember/object';
|
import { computed } from '@ember/object';
|
||||||
import { inject as service } from '@ember/service';
|
import { service } from '@ember/service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base model class provides common functionality for all models.
|
* Base model class provides common functionality for all models.
|
||||||
|
|||||||
@@ -4,14 +4,24 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import GeneratedCredentialLibraryModel from '../generated/models/credential-library';
|
import GeneratedCredentialLibraryModel from '../generated/models/credential-library';
|
||||||
|
import {
|
||||||
|
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
|
||||||
|
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
|
||||||
|
TYPE_CREDENTIAL_USERNAME_PASSWORD,
|
||||||
|
TYPE_CREDENTIAL_PASSWORD,
|
||||||
|
} from 'api/models/credential';
|
||||||
/**
|
/**
|
||||||
* Enum options for credential library.
|
* Enum options for credential library.
|
||||||
*/
|
*/
|
||||||
export const options = {
|
export const options = {
|
||||||
http_method: ['GET', 'POST'],
|
http_method: ['GET', 'POST'],
|
||||||
key_type: ['ed25519', 'ecdsa', 'rsa'],
|
key_type: ['ed25519', 'ecdsa', 'rsa'],
|
||||||
credential_types: ['ssh_private_key', 'username_password'],
|
credential_types: [
|
||||||
|
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
|
||||||
|
TYPE_CREDENTIAL_USERNAME_PASSWORD,
|
||||||
|
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
|
||||||
|
TYPE_CREDENTIAL_PASSWORD,
|
||||||
|
],
|
||||||
mapping_overrides: {
|
mapping_overrides: {
|
||||||
username_password: ['username_attribute', 'password_attribute'],
|
username_password: ['username_attribute', 'password_attribute'],
|
||||||
ssh_private_key: [
|
ssh_private_key: [
|
||||||
@@ -19,12 +29,19 @@ export const options = {
|
|||||||
'private_key_passphrase_attribute',
|
'private_key_passphrase_attribute',
|
||||||
'username_attribute',
|
'username_attribute',
|
||||||
],
|
],
|
||||||
|
username_password_domain: [
|
||||||
|
'username_attribute',
|
||||||
|
'password_attribute',
|
||||||
|
'domain_attribute',
|
||||||
|
],
|
||||||
|
password: ['password_attribute'],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC = 'vault-generic';
|
export const TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC = 'vault-generic';
|
||||||
export const TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE =
|
export const TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE =
|
||||||
'vault-ssh-certificate';
|
'vault-ssh-certificate';
|
||||||
|
export const TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP = 'vault-ldap';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Supported Credential Library types.
|
* Supported Credential Library types.
|
||||||
@@ -32,6 +49,7 @@ export const TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE =
|
|||||||
export const TYPES_CREDENTIAL_LIBRARY = Object.freeze([
|
export const TYPES_CREDENTIAL_LIBRARY = Object.freeze([
|
||||||
TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
|
TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
|
||||||
TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
|
TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
|
||||||
|
TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export default class CredentialLibraryModel extends GeneratedCredentialLibraryModel {
|
export default class CredentialLibraryModel extends GeneratedCredentialLibraryModel {
|
||||||
@@ -52,7 +70,8 @@ export default class CredentialLibraryModel extends GeneratedCredentialLibraryMo
|
|||||||
get isVault() {
|
get isVault() {
|
||||||
return (
|
return (
|
||||||
this.type === TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC ||
|
this.type === TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC ||
|
||||||
this.type === TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE
|
this.type === TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE ||
|
||||||
|
this.type === TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,4 +90,12 @@ export default class CredentialLibraryModel extends GeneratedCredentialLibraryMo
|
|||||||
get isVaultSSHCertificate() {
|
get isVaultSSHCertificate() {
|
||||||
return this.type === TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE;
|
return this.type === TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if credential is a vault ldap type.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
get isVaultLDAP() {
|
||||||
|
return this.type === TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,21 @@ import GeneratedCredentialModel from '../generated/models/credential';
|
|||||||
/**
|
/**
|
||||||
* Supported Credential types.
|
* Supported Credential types.
|
||||||
*/
|
*/
|
||||||
export const types = ['username_password', 'ssh_private_key', 'json'];
|
|
||||||
|
export const TYPE_CREDENTIAL_USERNAME_PASSWORD = 'username_password';
|
||||||
|
export const TYPE_CREDENTIAL_SSH_PRIVATE_KEY = 'ssh_private_key';
|
||||||
|
export const TYPE_CREDENTIAL_JSON = 'json';
|
||||||
|
export const TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN =
|
||||||
|
'username_password_domain';
|
||||||
|
export const TYPE_CREDENTIAL_PASSWORD = 'password';
|
||||||
|
|
||||||
|
export const TYPES_CREDENTIAL = Object.freeze([
|
||||||
|
TYPE_CREDENTIAL_USERNAME_PASSWORD,
|
||||||
|
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
|
||||||
|
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
|
||||||
|
TYPE_CREDENTIAL_JSON,
|
||||||
|
TYPE_CREDENTIAL_PASSWORD,
|
||||||
|
]);
|
||||||
|
|
||||||
export default class CredentialModel extends GeneratedCredentialModel {
|
export default class CredentialModel extends GeneratedCredentialModel {
|
||||||
// =attributes
|
// =attributes
|
||||||
@@ -28,7 +42,7 @@ export default class CredentialModel extends GeneratedCredentialModel {
|
|||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
get isJSON() {
|
get isJSON() {
|
||||||
return this.type === 'json';
|
return this.type === TYPE_CREDENTIAL_JSON;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,6 +50,6 @@ export default class CredentialModel extends GeneratedCredentialModel {
|
|||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
get isUnknown() {
|
get isUnknown() {
|
||||||
return !types.includes(this.type);
|
return !TYPES_CREDENTIAL.includes(this.type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,7 @@ import GeneratedGroupModel from '../generated/models/group';
|
|||||||
import { attr } from '@ember-data/model';
|
import { attr } from '@ember-data/model';
|
||||||
import { computed } from '@ember/object';
|
import { computed } from '@ember/object';
|
||||||
import { A } from '@ember/array';
|
import { A } from '@ember/array';
|
||||||
import { inject as service } from '@ember/service';
|
import { service } from '@ember/service';
|
||||||
|
|
||||||
export default class GroupModel extends GeneratedGroupModel {
|
export default class GroupModel extends GeneratedGroupModel {
|
||||||
// =services
|
// =services
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import GeneratedHostCatalogModel from '../generated/models/host-catalog';
|
import GeneratedHostCatalogModel from '../generated/models/host-catalog';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
|
||||||
export const TYPE_HOST_CATALOG_STATIC = 'static';
|
export const TYPE_HOST_CATALOG_STATIC = 'static';
|
||||||
export const TYPE_HOST_CATALOG_DYNAMIC = 'plugin';
|
export const TYPE_HOST_CATALOG_DYNAMIC = 'plugin';
|
||||||
@@ -14,9 +15,11 @@ export const TYPES_HOST_CATALOG = Object.freeze([
|
|||||||
|
|
||||||
export const TYPE_HOST_CATALOG_PLUGIN_AWS = 'aws';
|
export const TYPE_HOST_CATALOG_PLUGIN_AWS = 'aws';
|
||||||
export const TYPE_HOST_CATALOG_PLUGIN_AZURE = 'azure';
|
export const TYPE_HOST_CATALOG_PLUGIN_AZURE = 'azure';
|
||||||
|
export const TYPE_HOST_CATALOG_PLUGIN_GCP = 'gcp';
|
||||||
export const TYPES_HOST_CATALOG_PLUGIN = [
|
export const TYPES_HOST_CATALOG_PLUGIN = [
|
||||||
TYPE_HOST_CATALOG_PLUGIN_AWS,
|
TYPE_HOST_CATALOG_PLUGIN_AWS,
|
||||||
TYPE_HOST_CATALOG_PLUGIN_AZURE,
|
TYPE_HOST_CATALOG_PLUGIN_AZURE,
|
||||||
|
TYPE_HOST_CATALOG_PLUGIN_GCP,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const TYPE_CREDENTIAL_STATIC = 'static-credential';
|
export const TYPE_CREDENTIAL_STATIC = 'static-credential';
|
||||||
@@ -27,10 +30,19 @@ export const TYPES_CREDENTIALS = Object.freeze([
|
|||||||
TYPE_CREDENTIAL_DYNAMIC,
|
TYPE_CREDENTIAL_DYNAMIC,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export const DYNAMIC_CREDENTIAL_FIELDS = [
|
||||||
|
'role_arn',
|
||||||
|
'role_external_id',
|
||||||
|
'role_session_name',
|
||||||
|
'role_tags',
|
||||||
|
];
|
||||||
|
export const STATIC_CREDENTIAL_FIELDS = ['access_key_id, secret_access_key'];
|
||||||
|
|
||||||
export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
||||||
// =attributes
|
// =attributes
|
||||||
|
|
||||||
#credentialType;
|
@tracked _credentialTypeValue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True if the host catalog is static.
|
* True if the host catalog is static.
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
@@ -52,14 +64,14 @@ export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
|||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
get credentialType() {
|
get credentialType() {
|
||||||
if (!this.#credentialType) {
|
if (!this._credentialTypeValue) {
|
||||||
if (this.role_arn) {
|
if (DYNAMIC_CREDENTIAL_FIELDS.some((field) => this[field])) {
|
||||||
this.#credentialType = TYPE_CREDENTIAL_DYNAMIC;
|
return TYPE_CREDENTIAL_DYNAMIC;
|
||||||
} else {
|
} else {
|
||||||
this.#credentialType = TYPE_CREDENTIAL_STATIC;
|
return TYPE_CREDENTIAL_STATIC;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.#credentialType;
|
return this._credentialTypeValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,7 +79,7 @@ export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
|||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
set credentialType(type) {
|
set credentialType(type) {
|
||||||
this.#credentialType = type;
|
this._credentialTypeValue = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -85,7 +97,7 @@ export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
|||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
get isAWS() {
|
get isAWS() {
|
||||||
return this.compositeType === 'aws';
|
return this.compositeType === TYPE_HOST_CATALOG_PLUGIN_AWS;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -93,7 +105,15 @@ export default class HostCatalogModel extends GeneratedHostCatalogModel {
|
|||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
get isAzure() {
|
get isAzure() {
|
||||||
return this.compositeType === 'azure';
|
return this.compositeType === TYPE_HOST_CATALOG_PLUGIN_AZURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if host catalog plugin type is GCP.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
get isGCP() {
|
||||||
|
return this.compositeType === TYPE_HOST_CATALOG_PLUGIN_GCP;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,7 +4,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import GeneratedHostSetModel from '../generated/models/host-set';
|
import GeneratedHostSetModel from '../generated/models/host-set';
|
||||||
import { TYPES_HOST_CATALOG_PLUGIN } from './host-catalog';
|
import {
|
||||||
|
TYPES_HOST_CATALOG_PLUGIN,
|
||||||
|
TYPE_HOST_CATALOG_PLUGIN_GCP,
|
||||||
|
TYPE_HOST_CATALOG_PLUGIN_AWS,
|
||||||
|
TYPE_HOST_CATALOG_PLUGIN_AZURE,
|
||||||
|
} from './host-catalog';
|
||||||
|
|
||||||
export default class HostSetModel extends GeneratedHostSetModel {
|
export default class HostSetModel extends GeneratedHostSetModel {
|
||||||
// =attributes
|
// =attributes
|
||||||
@@ -40,14 +45,22 @@ export default class HostSetModel extends GeneratedHostSetModel {
|
|||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
get isAWS() {
|
get isAWS() {
|
||||||
return this.compositeType === 'aws';
|
return this.compositeType === TYPE_HOST_CATALOG_PLUGIN_AWS;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return if a host-set plugin is Azure or not.
|
* Return if a host-set plugin is Azure or not.
|
||||||
*/
|
*/
|
||||||
get isAzure() {
|
get isAzure() {
|
||||||
return this.compositeType === 'azure';
|
return this.compositeType === TYPE_HOST_CATALOG_PLUGIN_AZURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if host catalog plugin type is GCP.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
get isGCP() {
|
||||||
|
return this.compositeType === TYPE_HOST_CATALOG_PLUGIN_GCP;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import GeneratedRoleModel from '../generated/models/role';
|
import GeneratedRoleModel from '../generated/models/role';
|
||||||
import { attr } from '@ember-data/model';
|
import { attr } from '@ember-data/model';
|
||||||
import { inject as service } from '@ember/service';
|
import { service } from '@ember/service';
|
||||||
|
|
||||||
export const GRANT_SCOPE_THIS = 'this';
|
export const GRANT_SCOPE_THIS = 'this';
|
||||||
export const GRANT_SCOPE_CHILDREN = 'children';
|
export const GRANT_SCOPE_CHILDREN = 'children';
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import GeneratedSessionRecordingModel from '../generated/models/session-recording';
|
import GeneratedSessionRecordingModel from '../generated/models/session-recording';
|
||||||
import { hasMany } from '@ember-data/model';
|
import { hasMany } from '@ember-data/model';
|
||||||
import { inject as service } from '@ember/service';
|
import { service } from '@ember/service';
|
||||||
|
|
||||||
export const TYPE_SESSION_RECORDING_SSH = 'ssh';
|
export const TYPE_SESSION_RECORDING_SSH = 'ssh';
|
||||||
export const TYPES_SESSION_RECORDING = Object.freeze([
|
export const TYPES_SESSION_RECORDING = Object.freeze([
|
||||||
@@ -71,7 +71,15 @@ export default class SessionRecordingModel extends GeneratedSessionRecordingMode
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reapply policy dates via the `reapply-staorage-policy` method.
|
* Returns true if retention of session-recording is forever.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
get retainForever() {
|
||||||
|
return this.retain_until?.toISOString() === '9999-12-31T23:23:23.999Z';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reapply policy dates via the `reapply-storage-policy` method.
|
||||||
* @param {object} options
|
* @param {object} options
|
||||||
* @param {object} options.adapterOptions
|
* @param {object} options.adapterOptions
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import GeneratedSessionModel from '../generated/models/session';
|
|||||||
import { equal } from '@ember/object/computed';
|
import { equal } from '@ember/object/computed';
|
||||||
import { tracked } from '@glimmer/tracking';
|
import { tracked } from '@glimmer/tracking';
|
||||||
import { A } from '@ember/array';
|
import { A } from '@ember/array';
|
||||||
import { inject as service } from '@ember/service';
|
import { service } from '@ember/service';
|
||||||
|
import { flattenObject } from '../utils/flatten-nested-object';
|
||||||
|
import { TYPES_CREDENTIAL_LIBRARY } from 'api/models/credential-library';
|
||||||
|
import { TYPE_CREDENTIAL_JSON } from 'api/models/credential';
|
||||||
|
|
||||||
export const STATUS_SESSION_ACTIVE = 'active';
|
export const STATUS_SESSION_ACTIVE = 'active';
|
||||||
export const STATUS_SESSION_PENDING = 'pending';
|
export const STATUS_SESSION_PENDING = 'pending';
|
||||||
@@ -23,7 +26,7 @@ export const statusTypes = [
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
class SessionCredential {
|
export class SessionCredential {
|
||||||
// =classes
|
// =classes
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,12 +37,14 @@ class SessionCredential {
|
|||||||
name;
|
name;
|
||||||
description;
|
description;
|
||||||
type;
|
type;
|
||||||
|
credentialtype;
|
||||||
|
|
||||||
constructor(id, name, description, type) {
|
constructor(id, name, description, type, credentialtype) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
|
this.credentialType = credentialtype;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -75,8 +80,10 @@ class SessionCredential {
|
|||||||
get secrets() {
|
get secrets() {
|
||||||
if (this.#payloadSecret?.decoded) {
|
if (this.#payloadSecret?.decoded) {
|
||||||
const secretJSON = this.#payloadSecret.decoded;
|
const secretJSON = this.#payloadSecret.decoded;
|
||||||
return Object.keys(secretJSON).map(
|
return this.extractSecrets(
|
||||||
(key) => new SessionCredential.SecretItem(key, secretJSON[key]),
|
secretJSON,
|
||||||
|
this.source.type,
|
||||||
|
this.source.credentialType,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// decode from base64
|
// decode from base64
|
||||||
@@ -85,11 +92,41 @@ class SessionCredential {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts secrets from the payload secret JSON object.
|
||||||
|
* We only need to flatten the object if the type is vault or if its a json type credential.
|
||||||
|
* @param {object} secretJSON - The payload secret JSON object.
|
||||||
|
* @param {string} type - The credential source type.
|
||||||
|
* @param {string} credentialType - The credential type.
|
||||||
|
* @returns {SessionCredential.SecretItem[]} - The array of secret items.
|
||||||
|
*/
|
||||||
|
extractSecrets(secretJSON, type, credentialType) {
|
||||||
|
let source;
|
||||||
|
if (credentialType === TYPE_CREDENTIAL_JSON) {
|
||||||
|
source = flattenObject(secretJSON);
|
||||||
|
} else if (TYPES_CREDENTIAL_LIBRARY.includes(type) && secretJSON?.data) {
|
||||||
|
source = flattenObject(secretJSON.data);
|
||||||
|
} else {
|
||||||
|
source = secretJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(source).map(
|
||||||
|
([key, value]) => new SessionCredential.SecretItem(key, value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// =methods
|
// =methods
|
||||||
|
|
||||||
constructor(cred) {
|
constructor(cred) {
|
||||||
const { id, name, description, type } = cred.credential_source;
|
const { id, name, description, type, credential_type } =
|
||||||
this.source = new SessionCredential.Source(id, name, description, type);
|
cred.credential_source;
|
||||||
|
this.source = new SessionCredential.Source(
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
type,
|
||||||
|
credential_type,
|
||||||
|
);
|
||||||
this.#payloadSecret = cred.secret;
|
this.#payloadSecret = cred.secret;
|
||||||
this.rawCredential = cred;
|
this.rawCredential = cred;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import GeneratedStorageBucketModel from '../generated/models/storage-bucket';
|
import GeneratedStorageBucketModel from '../generated/models/storage-bucket';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
|
||||||
export const TYPE_STORAGE_BUCKET_PLUGIN = 'plugin';
|
export const TYPE_STORAGE_BUCKET_PLUGIN = 'plugin';
|
||||||
export const TYPES_STORAGE_BUCKET = Object.freeze([TYPE_STORAGE_BUCKET_PLUGIN]);
|
export const TYPES_STORAGE_BUCKET = Object.freeze([TYPE_STORAGE_BUCKET_PLUGIN]);
|
||||||
@@ -26,7 +27,8 @@ export const TYPES_CREDENTIALS = Object.freeze([
|
|||||||
export default class StorageBucketModel extends GeneratedStorageBucketModel {
|
export default class StorageBucketModel extends GeneratedStorageBucketModel {
|
||||||
// attributes
|
// attributes
|
||||||
|
|
||||||
#credentialType;
|
@tracked _credentialTypeValue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True if the storage bucket is a plugin.
|
* True if the storage bucket is a plugin.
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
@@ -40,14 +42,14 @@ export default class StorageBucketModel extends GeneratedStorageBucketModel {
|
|||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
get credentialType() {
|
get credentialType() {
|
||||||
if (!this.#credentialType) {
|
if (!this._credentialTypeValue) {
|
||||||
if (this.role_arn) {
|
if (this.role_arn) {
|
||||||
this.#credentialType = TYPE_CREDENTIAL_DYNAMIC;
|
return TYPE_CREDENTIAL_DYNAMIC;
|
||||||
} else {
|
} else {
|
||||||
this.#credentialType = TYPE_CREDENTIAL_STATIC;
|
return TYPE_CREDENTIAL_STATIC;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.#credentialType;
|
return this._credentialTypeValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,8 +57,9 @@ export default class StorageBucketModel extends GeneratedStorageBucketModel {
|
|||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
set credentialType(type) {
|
set credentialType(type) {
|
||||||
this.#credentialType = type;
|
this._credentialTypeValue = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True if the storage bucket is an unknown type.
|
* True if the storage bucket is an unknown type.
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
|
|||||||
@@ -5,11 +5,17 @@
|
|||||||
|
|
||||||
import GeneratedTargetModel from '../generated/models/target';
|
import GeneratedTargetModel from '../generated/models/target';
|
||||||
import { attr } from '@ember-data/model';
|
import { attr } from '@ember-data/model';
|
||||||
import { inject as service } from '@ember/service';
|
import { service } from '@ember/service';
|
||||||
|
|
||||||
export const TYPE_TARGET_TCP = 'tcp';
|
export const TYPE_TARGET_TCP = 'tcp';
|
||||||
export const TYPE_TARGET_SSH = 'ssh';
|
export const TYPE_TARGET_SSH = 'ssh';
|
||||||
export const TYPES_TARGET = Object.freeze([TYPE_TARGET_TCP, TYPE_TARGET_SSH]);
|
export const TYPE_TARGET_RDP = 'rdp';
|
||||||
|
|
||||||
|
export const TYPES_TARGET = Object.freeze([
|
||||||
|
TYPE_TARGET_TCP,
|
||||||
|
TYPE_TARGET_SSH,
|
||||||
|
TYPE_TARGET_RDP,
|
||||||
|
]);
|
||||||
|
|
||||||
export default class TargetModel extends GeneratedTargetModel {
|
export default class TargetModel extends GeneratedTargetModel {
|
||||||
// =services
|
// =services
|
||||||
@@ -347,4 +353,12 @@ export default class TargetModel extends GeneratedTargetModel {
|
|||||||
get isSSH() {
|
get isSSH() {
|
||||||
return this.type === TYPE_TARGET_SSH;
|
return this.type === TYPE_TARGET_SSH;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the target type is rdp.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
get isRDP() {
|
||||||
|
return this.type === TYPE_TARGET_RDP;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -98,8 +98,9 @@ export default class AccountSerializer extends ApplicationSerializer {
|
|||||||
serialized.attributes = {};
|
serialized.attributes = {};
|
||||||
}
|
}
|
||||||
// New record case
|
// New record case
|
||||||
if (password && snapshot?.record?.isNew)
|
if (password && snapshot?.record?.isNew) {
|
||||||
serialized.attributes.password = password;
|
serialized.attributes.password = password;
|
||||||
|
}
|
||||||
// Set password custom method
|
// Set password custom method
|
||||||
if (snapshot?.adapterOptions?.method === 'set-password') {
|
if (snapshot?.adapterOptions?.method === 'set-password') {
|
||||||
serialized = this.serializeForSetPassword(snapshot, password);
|
serialized = this.serializeForSetPassword(snapshot, password);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import RESTSerializer from '@ember-data/serializer/rest';
|
import RESTSerializer from '@ember-data/serializer/rest';
|
||||||
import { underscore } from '@ember/string';
|
import { underscore } from '@ember/string';
|
||||||
import { get } from '@ember/object';
|
import { get } from '@ember/object';
|
||||||
|
import { typeOf } from '@ember/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages serialization/normalization of data to/from the API.
|
* Manages serialization/normalization of data to/from the API.
|
||||||
@@ -42,7 +43,7 @@ export default class ApplicationSerializer extends RESTSerializer {
|
|||||||
*/
|
*/
|
||||||
serializeAttribute(snapshot, json, key, attribute) {
|
serializeAttribute(snapshot, json, key, attribute) {
|
||||||
const { type, options } = attribute;
|
const { type, options } = attribute;
|
||||||
const { type: recordType } = snapshot.record;
|
const { type: recordType, compositeType } = snapshot.record;
|
||||||
let value = super.serializeAttribute(...arguments);
|
let value = super.serializeAttribute(...arguments);
|
||||||
// Remove secret attributes that are null or empty
|
// Remove secret attributes that are null or empty
|
||||||
if (options.isSecret) {
|
if (options.isSecret) {
|
||||||
@@ -59,6 +60,18 @@ export default class ApplicationSerializer extends RESTSerializer {
|
|||||||
// Do not serialize `disabled` fields.
|
// Do not serialize `disabled` fields.
|
||||||
// TODO: disabled is temporarily disabled
|
// TODO: disabled is temporarily disabled
|
||||||
if (key === 'disabled') delete json[key];
|
if (key === 'disabled') delete json[key];
|
||||||
|
|
||||||
|
// Before we transform our json
|
||||||
|
// Let's trim the values in the json obj
|
||||||
|
// Currently, json values will only ever be a string or an array
|
||||||
|
if (options.trimWhitespace && json[key]) {
|
||||||
|
if (typeOf(json[key]) === 'array') {
|
||||||
|
json[key] = json[key].map((str) => str.trim());
|
||||||
|
} else if (typeOf(json[key] === 'string')) {
|
||||||
|
json[key] = json[key].trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Push nested attributes down into the attributes key
|
// Push nested attributes down into the attributes key
|
||||||
if (options.isNestedAttribute && json[key] !== undefined) {
|
if (options.isNestedAttribute && json[key] !== undefined) {
|
||||||
if (!json.attributes) json.attributes = {};
|
if (!json.attributes) json.attributes = {};
|
||||||
@@ -74,18 +87,38 @@ export default class ApplicationSerializer extends RESTSerializer {
|
|||||||
delete json[key];
|
delete json[key];
|
||||||
}
|
}
|
||||||
// If an attribute has a `for` option, it must match the
|
// If an attribute has a `for` option, it must match the
|
||||||
// record's `type`, else the attribute is excluded
|
// record's `type`, otherwise the attribute is excluded
|
||||||
// from serialization.
|
// from serialization.
|
||||||
//note: This doesn't handle secrets yet, we can add support for them if needed.
|
|
||||||
if (
|
if (options?.for) {
|
||||||
options?.for &&
|
// Check the typeof the `for` option to determine if it is a plugin or not
|
||||||
options.for !== recordType &&
|
const isPlugin = options.for?.type === 'plugin';
|
||||||
!options.for.includes(recordType)
|
// Helper function to handle deleting attribute and secret keys
|
||||||
) {
|
const deleteKey = (json) => {
|
||||||
if (options.isNestedAttribute) {
|
if (options.isNestedAttribute) {
|
||||||
delete json?.attributes?.[key];
|
delete json?.attributes?.[key];
|
||||||
} else {
|
} else if (options.isNestedSecret && json?.secrets?.[key]) {
|
||||||
delete json[key];
|
delete json?.secrets?.[key];
|
||||||
|
if (Object.keys(json.secrets).length === 0) {
|
||||||
|
delete json.secrets;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete json[key];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// For plugins, the `for` option is an object with a `name` key
|
||||||
|
// For non-plugins, the `for` option can be string or an array
|
||||||
|
if (
|
||||||
|
(isPlugin && !options.for?.name.includes(compositeType)) ||
|
||||||
|
(!isPlugin &&
|
||||||
|
!options.for?.includes(recordType) &&
|
||||||
|
typeOf(options.for) === 'array') ||
|
||||||
|
(!isPlugin &&
|
||||||
|
options.for !== recordType &&
|
||||||
|
typeOf(options.for) === 'string')
|
||||||
|
) {
|
||||||
|
deleteKey(json);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
@@ -211,12 +244,10 @@ export default class ApplicationSerializer extends RESTSerializer {
|
|||||||
* @return {object}
|
* @return {object}
|
||||||
*/
|
*/
|
||||||
normalizeMissingArrays(store, primaryModelClass, payload) {
|
normalizeMissingArrays(store, primaryModelClass, payload) {
|
||||||
const attrDefs = store
|
const attrDefs = store.schema.fields({ type: primaryModelClass.modelName });
|
||||||
.getSchemaDefinitionService()
|
|
||||||
.attributesDefinitionFor({ type: primaryModelClass.modelName });
|
|
||||||
if (attrDefs) {
|
if (attrDefs) {
|
||||||
Object.keys(attrDefs).forEach((key) => {
|
attrDefs.keys().forEach((key) => {
|
||||||
if (!payload[key] && attrDefs[key]?.options?.emptyArrayIfMissing) {
|
if (!payload[key] && attrDefs.get(key)?.options?.emptyArrayIfMissing) {
|
||||||
payload[key] = [];
|
payload[key] = [];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -235,12 +266,10 @@ export default class ApplicationSerializer extends RESTSerializer {
|
|||||||
* @return {object}
|
* @return {object}
|
||||||
*/
|
*/
|
||||||
normalizeMissingObjects(store, primaryModelClass, payload) {
|
normalizeMissingObjects(store, primaryModelClass, payload) {
|
||||||
const attrDefs = store
|
const attrDefs = store.schema.fields({ type: primaryModelClass.modelName });
|
||||||
.getSchemaDefinitionService()
|
|
||||||
.attributesDefinitionFor({ type: primaryModelClass.modelName });
|
|
||||||
if (attrDefs) {
|
if (attrDefs) {
|
||||||
Object.keys(attrDefs).forEach((key) => {
|
attrDefs.keys().forEach((key) => {
|
||||||
if (!payload[key] && attrDefs[key]?.options?.emptyObjectIfMissing) {
|
if (!payload[key] && attrDefs.get(key)?.options?.emptyObjectIfMissing) {
|
||||||
payload[key] = {};
|
payload[key] = {};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export default class CredentialLibrarySerializer extends ApplicationSerializer {
|
|||||||
case TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC:
|
case TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC:
|
||||||
return this.serializeVaultGeneric(...arguments);
|
return this.serializeVaultGeneric(...arguments);
|
||||||
case TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE:
|
case TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE:
|
||||||
|
return this.serializeVaultSshCertificate(...arguments);
|
||||||
default:
|
default:
|
||||||
return super.serialize(...arguments);
|
return super.serialize(...arguments);
|
||||||
}
|
}
|
||||||
@@ -43,22 +44,45 @@ export default class CredentialLibrarySerializer extends ApplicationSerializer {
|
|||||||
if (!serialized.attributes?.http_method?.match(/post/i)) {
|
if (!serialized.attributes?.http_method?.match(/post/i)) {
|
||||||
serialized.attributes.http_request_body = null;
|
serialized.attributes.http_request_body = null;
|
||||||
}
|
}
|
||||||
const { credential_type, credential_mapping_overrides } = serialized;
|
this.handleCredentialMappingOverrides(serialized, isNew);
|
||||||
|
return serialized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// API expects to send null to fields if it is undefined or deleted
|
serializeVaultSshCertificate() {
|
||||||
if (credential_mapping_overrides && !isNew) {
|
const serialized = super.serialize(...arguments);
|
||||||
serialized.credential_mapping_overrides = options.mapping_overrides[
|
if (serialized.attributes) {
|
||||||
credential_type
|
const { key_type } = serialized.attributes;
|
||||||
].reduce((obj, key) => {
|
if (key_type !== 'rsa' && key_type !== 'ecdsa') {
|
||||||
if (credential_mapping_overrides[key]) {
|
// If the key type is not RSA or ECDSA, set key_bits to null
|
||||||
obj[key] = credential_mapping_overrides[key];
|
serialized.attributes.key_bits = null;
|
||||||
} else {
|
|
||||||
obj[key] = null;
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
}, {});
|
|
||||||
}
|
}
|
||||||
return serialized;
|
return serialized;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle credential mapping overrides for serialization.
|
||||||
|
* @param {Object} serialized - The serialized object.
|
||||||
|
* @param {boolean} isNew - Indicates if the record is new.
|
||||||
|
*/
|
||||||
|
handleCredentialMappingOverrides(serialized, isNew) {
|
||||||
|
const { credential_type, credential_mapping_overrides } = serialized;
|
||||||
|
if (Object.keys(credential_mapping_overrides).length === 0) {
|
||||||
|
serialized.credential_mapping_overrides = null;
|
||||||
|
}
|
||||||
|
// API expects to send null to fields if it is undefined or deleted
|
||||||
|
if (credential_mapping_overrides && !isNew) {
|
||||||
|
serialized.credential_mapping_overrides = options.mapping_overrides[
|
||||||
|
credential_type
|
||||||
|
].reduce((obj, key) => {
|
||||||
|
if (credential_mapping_overrides[key]) {
|
||||||
|
obj[key] = credential_mapping_overrides[key];
|
||||||
|
} else {
|
||||||
|
obj[key] = null;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -28,8 +28,9 @@ export default class CredentialStoreSerializer extends ApplicationSerializer {
|
|||||||
if (
|
if (
|
||||||
!serialized.attributes?.client_certificate_key &&
|
!serialized.attributes?.client_certificate_key &&
|
||||||
serialized.attributes?.client_certificate
|
serialized.attributes?.client_certificate
|
||||||
)
|
) {
|
||||||
delete serialized.attributes.client_certificate_key;
|
delete serialized.attributes.client_certificate_key;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return serialized;
|
return serialized;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,101 +4,45 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import ApplicationSerializer from './application';
|
import ApplicationSerializer from './application';
|
||||||
import {
|
import { TYPE_CREDENTIAL_DYNAMIC } from '../models/host-catalog';
|
||||||
TYPE_HOST_CATALOG_PLUGIN_AWS,
|
|
||||||
TYPE_HOST_CATALOG_PLUGIN_AZURE,
|
|
||||||
TYPE_CREDENTIAL_DYNAMIC,
|
|
||||||
} from '../models/host-catalog';
|
|
||||||
|
|
||||||
// These fields are Azure specific
|
|
||||||
const azureFields = [
|
|
||||||
'disable_credential_rotation',
|
|
||||||
'tenant_id',
|
|
||||||
'client_id',
|
|
||||||
'subscription_id',
|
|
||||||
'secret_id',
|
|
||||||
'secret_value',
|
|
||||||
];
|
|
||||||
|
|
||||||
// These fields are AWS specific, AWS provider now has two credentialTypes: static and dynamic [not to be confused with static and dynamic host-catalogs]
|
|
||||||
const AWSfieldsWithCredentialType = {
|
|
||||||
'static-credential': [
|
|
||||||
'access_key_id',
|
|
||||||
'secret_access_key',
|
|
||||||
'region',
|
|
||||||
'disable_credential_rotation',
|
|
||||||
'worker_filter',
|
|
||||||
],
|
|
||||||
'dynamic-credential': [
|
|
||||||
'role_arn',
|
|
||||||
'role_external_id',
|
|
||||||
'role_session_name',
|
|
||||||
'role_tags',
|
|
||||||
'region',
|
|
||||||
'disable_credential_rotation',
|
|
||||||
'worker_filter',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class HostCatalogSerializer extends ApplicationSerializer {
|
export default class HostCatalogSerializer extends ApplicationSerializer {
|
||||||
serialize(snapshot) {
|
serialize(snapshot) {
|
||||||
const { compositeType, credentialType } = snapshot.record;
|
const { credentialType } = snapshot.record;
|
||||||
const serialized = super.serialize(...arguments);
|
const serialized = super.serialize(...arguments);
|
||||||
|
|
||||||
// This is a hack to remove the worker_filter field from the serialized data if the compositeType is not AWS
|
|
||||||
// Ideally we should refactor the model to include the compositeType using `for` attribute
|
|
||||||
if (compositeType !== TYPE_HOST_CATALOG_PLUGIN_AWS) {
|
|
||||||
delete serialized.worker_filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
// By default, disable credential rotation for dynamic credentials
|
// By default, disable credential rotation for dynamic credentials
|
||||||
if (credentialType === TYPE_CREDENTIAL_DYNAMIC) {
|
if (credentialType === TYPE_CREDENTIAL_DYNAMIC) {
|
||||||
serialized.attributes.disable_credential_rotation = true;
|
serialized.attributes.disable_credential_rotation = true;
|
||||||
}
|
}
|
||||||
|
return serialized;
|
||||||
switch (compositeType) {
|
|
||||||
case 'static':
|
|
||||||
return this.serializeStatic(...arguments);
|
|
||||||
default:
|
|
||||||
return serialized;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
serializeAttribute(snapshot, json, key, attribute) {
|
serializeAttribute(snapshot, json, key, attribute) {
|
||||||
const value = super.serializeAttribute(...arguments);
|
const value = super.serializeAttribute(...arguments);
|
||||||
const { compositeType, credentialType, isPlugin } = snapshot.record;
|
const { compositeType, credentialType } = snapshot.record;
|
||||||
const { options } = attribute;
|
const { options } = attribute;
|
||||||
|
const isSamePluginName = options?.for?.name?.includes(compositeType);
|
||||||
|
|
||||||
const fields =
|
// Remove attributes that are not applicable to the current credential type for the same plugin
|
||||||
compositeType === TYPE_HOST_CATALOG_PLUGIN_AZURE
|
// For example, 'aws' dynamic plugin has two credential types: 'static' and 'dynamic'
|
||||||
? azureFields
|
// API expects the fields to be 'null' for the fields that are not from the current credential type (that's why we are not using the application serializer for this, where we delete attrs)
|
||||||
: AWSfieldsWithCredentialType[credentialType];
|
// Empty secrets should be removed from the JSON
|
||||||
|
if (
|
||||||
// Delete any nested attribute fields that don't belong to the record type
|
isSamePluginName &&
|
||||||
if (isPlugin && options.isNestedAttribute && json.attributes) {
|
options.for.credentialType &&
|
||||||
if (!fields.includes(key)) {
|
options.for.credentialType !== credentialType
|
||||||
delete json.attributes[key];
|
) {
|
||||||
}
|
if (options.isNestedAttribute) {
|
||||||
}
|
json.attributes[key] = null;
|
||||||
|
} else if (options.isNestedSecret && json.secrets?.[key]) {
|
||||||
// Delete any secret fields that don't belong to the record type
|
|
||||||
if (isPlugin && options.isNestedSecret && json.secrets) {
|
|
||||||
if (!fields.includes(key)) {
|
|
||||||
delete json.secrets[key];
|
delete json.secrets[key];
|
||||||
if (json['secrets'] && Object.keys(json.secrets).length === 0) {
|
if (Object.keys(json.secrets).length === 0) {
|
||||||
delete json.secrets;
|
delete json.secrets;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
delete json[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
serializeStatic() {
|
|
||||||
const serialized = super.serialize(...arguments);
|
|
||||||
// Delete unnecessary fields for static host-catalog
|
|
||||||
delete serialized.attributes;
|
|
||||||
delete serialized.secrets;
|
|
||||||
delete serialized.worker_filter;
|
|
||||||
return serialized;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -5,10 +5,6 @@
|
|||||||
|
|
||||||
import ApplicationSerializer from './application';
|
import ApplicationSerializer from './application';
|
||||||
|
|
||||||
const fieldByType = {
|
|
||||||
aws: ['preferred_endpoints', 'filters', 'sync_interval_seconds'],
|
|
||||||
azure: ['preferred_endpoints', 'filter_string', 'sync_interval_seconds'],
|
|
||||||
};
|
|
||||||
export default class HostSetSerializer extends ApplicationSerializer {
|
export default class HostSetSerializer extends ApplicationSerializer {
|
||||||
// =properties
|
// =properties
|
||||||
|
|
||||||
@@ -63,16 +59,8 @@ export default class HostSetSerializer extends ApplicationSerializer {
|
|||||||
return serialized;
|
return serialized;
|
||||||
}
|
}
|
||||||
|
|
||||||
serializeAttribute(snapshot, json, key, attribute) {
|
serializeAttribute(snapshot, json, key) {
|
||||||
const value = super.serializeAttribute(...arguments);
|
const value = super.serializeAttribute(...arguments);
|
||||||
const { isPlugin, compositeType } = snapshot.record;
|
|
||||||
const { options } = attribute;
|
|
||||||
|
|
||||||
if (isPlugin && options.isNestedAttribute) {
|
|
||||||
if (!fieldByType[compositeType].includes(key)) {
|
|
||||||
delete json.attributes[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === 'filter_string') {
|
if (key === 'filter_string') {
|
||||||
const { filter_string } = json.attributes;
|
const { filter_string } = json.attributes;
|
||||||
@@ -107,4 +95,18 @@ export default class HostSetSerializer extends ApplicationSerializer {
|
|||||||
|
|
||||||
return super.normalize(typeClass, normalizedHash, ...rest);
|
return super.normalize(typeClass, normalizedHash, ...rest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts errors from the payload and transforms specific keys if needed.
|
||||||
|
* @override
|
||||||
|
* @return {object}
|
||||||
|
*/
|
||||||
|
extractErrors() {
|
||||||
|
const errors = super.extractErrors(...arguments);
|
||||||
|
if (errors?.filter) {
|
||||||
|
errors.filter_string = errors.filter;
|
||||||
|
delete errors.filter;
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -53,4 +53,18 @@ export default class ManagedGroupSerializer extends ApplicationSerializer {
|
|||||||
|
|
||||||
return super.normalize(typeClass, normalizedHash, ...rest);
|
return super.normalize(typeClass, normalizedHash, ...rest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts errors from the payload and transforms specific keys if needed.
|
||||||
|
* @override
|
||||||
|
* @return {object}
|
||||||
|
*/
|
||||||
|
extractErrors() {
|
||||||
|
const errors = super.extractErrors(...arguments);
|
||||||
|
if (errors?.filter) {
|
||||||
|
errors.filter_string = errors.filter;
|
||||||
|
delete errors.filter;
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -72,12 +72,14 @@ export default class StorageBucketSerializer extends ApplicationSerializer {
|
|||||||
const value = super.serializeAttribute(...arguments);
|
const value = super.serializeAttribute(...arguments);
|
||||||
const { credentialType } = snapshot.record;
|
const { credentialType } = snapshot.record;
|
||||||
const { options } = attribute;
|
const { options } = attribute;
|
||||||
|
|
||||||
// This deletes any fields that don't belong to the record's credential type
|
// This deletes any fields that don't belong to the record's credential type
|
||||||
if (options.isNestedAttribute && json.attributes) {
|
if (options.isNestedAttribute && json.attributes) {
|
||||||
// The key must be included in the fieldsByType list above
|
// The key must be included in the fieldsByType list above
|
||||||
if (!fieldsByCredentialType[credentialType].includes(key))
|
if (!fieldsByCredentialType[credentialType].includes(key)) {
|
||||||
// API requires these fields to be null
|
// API requires these fields to be null
|
||||||
json.attributes[key] = null;
|
json.attributes[key] = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//json.secrets is only present, if there are any updates to the secret fields
|
//json.secrets is only present, if there are any updates to the secret fields
|
||||||
|
|||||||
@@ -33,16 +33,18 @@ export default class TargetSerializer extends ApplicationSerializer {
|
|||||||
const injectedApplicationCredentialSourceIDs =
|
const injectedApplicationCredentialSourceIDs =
|
||||||
snapshot?.adapterOptions?.injectedApplicationCredentialSourceIDs;
|
snapshot?.adapterOptions?.injectedApplicationCredentialSourceIDs;
|
||||||
|
|
||||||
if (brokeredCredentialSourceIDs)
|
if (brokeredCredentialSourceIDs) {
|
||||||
serialized = this.serializeWithBrokeredCredentialSources(
|
serialized = this.serializeWithBrokeredCredentialSources(
|
||||||
snapshot,
|
snapshot,
|
||||||
brokeredCredentialSourceIDs,
|
brokeredCredentialSourceIDs,
|
||||||
);
|
);
|
||||||
if (injectedApplicationCredentialSourceIDs)
|
}
|
||||||
|
if (injectedApplicationCredentialSourceIDs) {
|
||||||
serialized = this.serializeWithInjectedApplicationCredentialSources(
|
serialized = this.serializeWithInjectedApplicationCredentialSources(
|
||||||
snapshot,
|
snapshot,
|
||||||
injectedApplicationCredentialSourceIDs,
|
injectedApplicationCredentialSourceIDs,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isNew && serialized?.with_aliases) {
|
if (isNew && serialized?.with_aliases) {
|
||||||
// API expects scope id along with every alias value
|
// API expects scope id along with every alias value
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ export default class UserSerializer extends ApplicationSerializer {
|
|||||||
serialize(snapshot) {
|
serialize(snapshot) {
|
||||||
let serialized = super.serialize(...arguments);
|
let serialized = super.serialize(...arguments);
|
||||||
const accountIDs = snapshot?.adapterOptions?.accountIDs;
|
const accountIDs = snapshot?.adapterOptions?.accountIDs;
|
||||||
if (accountIDs)
|
if (accountIDs) {
|
||||||
serialized = this.serializewithAccounts(snapshot, accountIDs);
|
serialized = this.serializewithAccounts(snapshot, accountIDs);
|
||||||
|
}
|
||||||
return serialized;
|
return serialized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Service, { service } from '@ember/service';
|
||||||
|
import { modelMapping } from 'api/services/sqlite';
|
||||||
|
import { assert } from '@ember/debug';
|
||||||
|
import { generateSQLExpressions } from '../utils/sqlite-query';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database service for querying Boundary resources from storage.
|
||||||
|
* Provides a high-level interface for executing queries against the local database.
|
||||||
|
*/
|
||||||
|
export default class DbService extends Service {
|
||||||
|
@service sqlite;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query resources from the database by type.
|
||||||
|
*
|
||||||
|
* @param {string} type - The resource type (e.g., 'scope', 'user', 'target').
|
||||||
|
* Must be a supported model type defined in modelMapping.
|
||||||
|
* @param {Object} query - Query configuration object
|
||||||
|
* @returns {Promise<Object>} Promise resolving to query results from sqlite service
|
||||||
|
* @throws {Error} Assertion error if resource type is not supported
|
||||||
|
*/
|
||||||
|
query(type, query) {
|
||||||
|
const supportedModels = Object.keys(modelMapping);
|
||||||
|
assert('Resource type is not supported.', supportedModels.includes(type));
|
||||||
|
|
||||||
|
let { page, pageSize, select, query: queryObj } = query;
|
||||||
|
|
||||||
|
const { sql, parameters } = generateSQLExpressions(type, queryObj, {
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
select,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.sqlite.fetchResource({
|
||||||
|
sql,
|
||||||
|
parameters,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) HashiCorp, Inc.
|
|
||||||
* SPDX-License-Identifier: BUSL-1.1
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Service from '@ember/service';
|
|
||||||
import Dexie from 'dexie';
|
|
||||||
import { typeOf } from '@ember/utils';
|
|
||||||
|
|
||||||
// List of resources we support to be cached and searched in
|
|
||||||
// indexedDb. Any field that can be searched upon should be added to
|
|
||||||
// this index. Increment the version number of the database whenever
|
|
||||||
// indexes change.
|
|
||||||
export const modelIndexes = {
|
|
||||||
token: '&id, token',
|
|
||||||
target:
|
|
||||||
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.address, attributes.scope.scope_id',
|
|
||||||
session:
|
|
||||||
'&id, attributes.created_time, attributes.type, attributes.status, attributes.endpoint, attributes.target_id, attributes.user_id, attributes.scope.scope_id',
|
|
||||||
user: '&id, attributes.created_time, attributes.name, attributes.description, attributes.scope.scope_id',
|
|
||||||
group:
|
|
||||||
'&id, attributes.created_time, attributes.name, attributes.description, attributes.scope.scope_id',
|
|
||||||
role: '&id, attributes.created_time, attributes.name, attributes.description, attributes.scope.scope_id',
|
|
||||||
scope:
|
|
||||||
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.scope.scope_id',
|
|
||||||
'credential-store':
|
|
||||||
'&id, attributes.created_time, attributes.name, attributes.description, attributes.type, attributes.scope.scope_id',
|
|
||||||
'host-catalog':
|
|
||||||
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.scope.scope_id, attributes.plugin.name',
|
|
||||||
'auth-method':
|
|
||||||
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.is_primary, attributes.scope.scope_id',
|
|
||||||
'session-recording':
|
|
||||||
'&id, attributes.created_time, attributes.type, attributes.state, attributes.start_time, attributes.end_time, attributes.duration, attributes.scope.scope_id, attributes.create_time_values.user.id, attributes.create_time_values.user.name, attributes.create_time_values.target.id, attributes.create_time_values.target.name, attributes.create_time_values.target.scope.id, attributes.create_time_values.target.scope.name',
|
|
||||||
alias:
|
|
||||||
'&id, attributes.created_time, attributes.type, attributes.value, attributes.name, attributes.description, attributes.destination_id, attributes.scope.scope_id',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const formatDbName = (userId, clusterUrl) =>
|
|
||||||
`boundary-${userId}-${clusterUrl}`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively copy over the object and convert any booleans to "true" or "false" so they can be indexed.
|
|
||||||
* Also converts any ember-like arrays back to a standard array.
|
|
||||||
* @param result
|
|
||||||
* @param object
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const convertFields = (result, object) => {
|
|
||||||
Object.entries(object).forEach(([key, value]) => {
|
|
||||||
if (typeOf(value) === 'object') {
|
|
||||||
result[key] = convertFields({}, value);
|
|
||||||
} else if (typeOf(value) === 'boolean') {
|
|
||||||
result[key] = value ? 'true' : 'false';
|
|
||||||
} else if (typeOf(value) === 'array') {
|
|
||||||
result[key] = [...value];
|
|
||||||
} else {
|
|
||||||
result[key] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively copy over the object and convert any string "true" or "false" back to booleans.
|
|
||||||
* Will also convert any array fields back using their model transformation.
|
|
||||||
* @param result
|
|
||||||
* @param object
|
|
||||||
* @param schema
|
|
||||||
* @param serializer
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const unconvertFields = (result, object, schema, serializer) => {
|
|
||||||
Object.entries(object).forEach(([key, value]) => {
|
|
||||||
if (typeOf(value) === 'object') {
|
|
||||||
result[key] = unconvertFields({}, value, schema, serializer);
|
|
||||||
} else if (value === 'true' || value === 'false') {
|
|
||||||
result[key] = value === 'true';
|
|
||||||
} else if (typeOf(value) === 'array') {
|
|
||||||
const attribute = schema.attributes.get(key);
|
|
||||||
|
|
||||||
// If attribute type is array, we transform it back to a tracked array
|
|
||||||
if (attribute?.type === 'array') {
|
|
||||||
const transformer = serializer.transformFor(attribute.type);
|
|
||||||
result[key] = transformer.deserialize(value, attribute.options);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
result[key] = value;
|
|
||||||
} else {
|
|
||||||
result[key] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service to encapsulate the IndexedDB implementation. To use this service, call
|
|
||||||
* `setup` from the application root and
|
|
||||||
*/
|
|
||||||
export default class IndexedDbService extends Service {
|
|
||||||
// =attributes
|
|
||||||
#db;
|
|
||||||
get db() {
|
|
||||||
return this.#db;
|
|
||||||
}
|
|
||||||
|
|
||||||
setup(dbName) {
|
|
||||||
// Don't run setup again if we already have one or if we didn't get a name
|
|
||||||
if (this.#db || !dbName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#db = new Dexie(dbName);
|
|
||||||
this.#db.version(2).stores(modelIndexes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleanup the data by converting all fields to plain javascript objects.
|
|
||||||
* We will also convert booleans to "false" or "true".
|
|
||||||
* IndexedDB doesn't support indexes on boolean values so we have to
|
|
||||||
* convert them to be able to support using boolean values.
|
|
||||||
*
|
|
||||||
* Undo the operation by setting `convertFields` to false.
|
|
||||||
*
|
|
||||||
* @param {Object} data
|
|
||||||
* @param {Boolean} cleanData
|
|
||||||
* @param {Object} schema
|
|
||||||
* @param {Object} serializer
|
|
||||||
* @return {{id, type, attributes: {}, relationships: {}}}
|
|
||||||
*/
|
|
||||||
normalizeData({ data, cleanData, schema, serializer }) {
|
|
||||||
if (!data) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { id, type, attributes = {}, relationships = {} } = data;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
type,
|
|
||||||
attributes: cleanData
|
|
||||||
? convertFields({}, attributes)
|
|
||||||
: unconvertFields({}, attributes, schema, serializer),
|
|
||||||
relationships: cleanData
|
|
||||||
? convertFields({}, relationships)
|
|
||||||
: unconvertFields({}, relationships, schema, serializer),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Service from '@ember/service';
|
import Service from '@ember/service';
|
||||||
import { inject as service } from '@ember/service';
|
import { service } from '@ember/service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes a POJO representing a query and converts it to a boolean expression
|
* Takes a POJO representing a query and converts it to a boolean expression
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Service from '@ember/service';
|
||||||
|
import { PWBHost } from 'promise-worker-bi';
|
||||||
|
|
||||||
|
export const formatDbName = (userId, clusterUrl) =>
|
||||||
|
`boundary-${userId}-${clusterUrl}`;
|
||||||
|
|
||||||
|
// A mapping of columns to their expected model attributes for supported models
|
||||||
|
// The JSON data column is not included here but is assumed to always be last.
|
||||||
|
export const modelMapping = {
|
||||||
|
target: {
|
||||||
|
id: 'id',
|
||||||
|
type: 'type',
|
||||||
|
name: 'name',
|
||||||
|
description: 'description',
|
||||||
|
address: 'address',
|
||||||
|
scope_id: 'scope.scope_id',
|
||||||
|
created_time: 'created_time',
|
||||||
|
},
|
||||||
|
alias: {
|
||||||
|
id: 'id',
|
||||||
|
type: 'type',
|
||||||
|
name: 'name',
|
||||||
|
description: 'description',
|
||||||
|
destination_id: 'destination_id',
|
||||||
|
value: 'value',
|
||||||
|
scope_id: 'scope.scope_id',
|
||||||
|
created_time: 'created_time',
|
||||||
|
},
|
||||||
|
group: {
|
||||||
|
id: 'id',
|
||||||
|
name: 'name',
|
||||||
|
description: 'description',
|
||||||
|
scope_id: 'scope.scope_id',
|
||||||
|
created_time: 'created_time',
|
||||||
|
},
|
||||||
|
role: {
|
||||||
|
id: 'id',
|
||||||
|
name: 'name',
|
||||||
|
description: 'description',
|
||||||
|
scope_id: 'scope.scope_id',
|
||||||
|
created_time: 'created_time',
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
id: 'id',
|
||||||
|
name: 'name',
|
||||||
|
description: 'description',
|
||||||
|
scope_id: 'scope.scope_id',
|
||||||
|
created_time: 'created_time',
|
||||||
|
},
|
||||||
|
'credential-store': {
|
||||||
|
id: 'id',
|
||||||
|
type: 'type',
|
||||||
|
name: 'name',
|
||||||
|
description: 'description',
|
||||||
|
scope_id: 'scope.scope_id',
|
||||||
|
created_time: 'created_time',
|
||||||
|
},
|
||||||
|
scope: {
|
||||||
|
id: 'id',
|
||||||
|
type: 'type',
|
||||||
|
name: 'name',
|
||||||
|
description: 'description',
|
||||||
|
scope_id: 'scope.scope_id',
|
||||||
|
created_time: 'created_time',
|
||||||
|
},
|
||||||
|
'auth-method': {
|
||||||
|
id: 'id',
|
||||||
|
type: 'type',
|
||||||
|
name: 'name',
|
||||||
|
description: 'description',
|
||||||
|
is_primary: 'is_primary',
|
||||||
|
scope_id: 'scope.scope_id',
|
||||||
|
created_time: 'created_time',
|
||||||
|
},
|
||||||
|
'host-catalog': {
|
||||||
|
id: 'id',
|
||||||
|
type: 'type',
|
||||||
|
name: 'name',
|
||||||
|
description: 'description',
|
||||||
|
plugin_name: 'plugin.name',
|
||||||
|
scope_id: 'scope.scope_id',
|
||||||
|
created_time: 'created_time',
|
||||||
|
},
|
||||||
|
'session-recording': {
|
||||||
|
id: 'id',
|
||||||
|
type: 'type',
|
||||||
|
state: 'state',
|
||||||
|
start_time: 'start_time',
|
||||||
|
end_time: 'end_time',
|
||||||
|
duration: 'duration',
|
||||||
|
scope_id: 'scope.scope_id',
|
||||||
|
user_id: 'create_time_values.user.id',
|
||||||
|
user_name: 'create_time_values.user.name',
|
||||||
|
target_id: 'create_time_values.target.id',
|
||||||
|
target_name: 'create_time_values.target.name',
|
||||||
|
target_scope_id: 'create_time_values.target.scope.id',
|
||||||
|
target_scope_name: 'create_time_values.target.scope.name',
|
||||||
|
target_scope_parent_scope_id:
|
||||||
|
'create_time_values.target.scope.parent_scope_id',
|
||||||
|
created_time: 'created_time',
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
id: 'id',
|
||||||
|
type: 'type',
|
||||||
|
status: 'status',
|
||||||
|
endpoint: 'endpoint',
|
||||||
|
target_id: 'target_id',
|
||||||
|
user_id: 'user_id',
|
||||||
|
scope_id: 'scope.scope_id',
|
||||||
|
created_time: 'created_time',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class SqliteDbService extends Service {
|
||||||
|
// =attributes
|
||||||
|
|
||||||
|
// We need a handle on the original web worker to properly clean it up.
|
||||||
|
webWorker;
|
||||||
|
// This will be the worker we interact with that is wrapped by PWBHost.
|
||||||
|
worker;
|
||||||
|
|
||||||
|
setup(dbName) {
|
||||||
|
if (this.worker) {
|
||||||
|
// If the worker already exists, we don't need to do anything.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In production, our JS files will be fingerprinted with an MD5 hash
|
||||||
|
// so we need a way to map the worker file name to be able to find it.
|
||||||
|
//
|
||||||
|
// Ember is automagically replacing any URL path with the fingerprinted
|
||||||
|
// asset, and this is done by broccoli-asset-rev.
|
||||||
|
//
|
||||||
|
// The problem is it's a bit finicky as seen in this issue
|
||||||
|
// https://github.com/ember-cli/broccoli-asset-rewrite/issues/66
|
||||||
|
// and will not work properly if it's part of a template string or interpolated.
|
||||||
|
// Therefore, we need the URL path in a string somewhere to be replaced properly.
|
||||||
|
this.webWorker = new Worker('/workers/sqlite-worker.js', {
|
||||||
|
type: 'module',
|
||||||
|
});
|
||||||
|
|
||||||
|
// We use PWBHost to simplify communication so we can just send and receive
|
||||||
|
// messages with promises rather than having to manually set up event listeners
|
||||||
|
this.worker = new PWBHost(this.webWorker);
|
||||||
|
this.worker.register(async ({ method }) => {
|
||||||
|
if (method === 'getDatabaseName') {
|
||||||
|
return dbName;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchResource({ sql, parameters }) {
|
||||||
|
return this.worker.postMessage({
|
||||||
|
method: 'fetchResource',
|
||||||
|
payload: { sql, parameters },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
insertResource(resource, items) {
|
||||||
|
return this.worker.postMessage({
|
||||||
|
method: 'insertResource',
|
||||||
|
payload: { resource, items, modelMapping },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteResource(resource, ids) {
|
||||||
|
return this.worker.postMessage({
|
||||||
|
method: 'deleteResource',
|
||||||
|
payload: { resource, ids },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
analyzeDatabase() {
|
||||||
|
return this.worker.postMessage({ method: 'analyzeDatabase' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadDatabase() {
|
||||||
|
const byteArray = await this.worker.postMessage({
|
||||||
|
method: 'downloadDatabase',
|
||||||
|
});
|
||||||
|
|
||||||
|
const blob = new Blob([byteArray.buffer], {
|
||||||
|
type: 'application/x-sqlite3',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create temporary elements to initialize the download
|
||||||
|
const a = window.document.createElement('a');
|
||||||
|
window.document.body.appendChild(a);
|
||||||
|
a.href = window.URL.createObjectURL(blob);
|
||||||
|
a.download = 'boundary.db';
|
||||||
|
a.addEventListener('click', function () {
|
||||||
|
setTimeout(function () {
|
||||||
|
window.URL.revokeObjectURL(a.href);
|
||||||
|
a.remove();
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearDatabase() {
|
||||||
|
await this.worker.postMessage({ method: 'clearDatabase' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteDatabase() {
|
||||||
|
await this.worker.postMessage({ method: 'deleteDatabase' });
|
||||||
|
}
|
||||||
|
|
||||||
|
willDestroy() {
|
||||||
|
this.webWorker?.terminate();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,7 +56,7 @@ class MemoryStorage {
|
|||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* import Route from '@ember/routing/route';
|
* import Route from '@ember/routing/route';
|
||||||
* import { inject as service } from '@ember/service';
|
* import { service } from '@ember/service';
|
||||||
* export default class ApplicationRoute extends Route {
|
* export default class ApplicationRoute extends Route {
|
||||||
* @service storage;
|
* @service storage;
|
||||||
* afterModel() {
|
* afterModel() {
|
||||||
|
|||||||
@@ -3,10 +3,14 @@
|
|||||||
* SPDX-License-Identifier: BUSL-1.1
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Store, { CacheHandler } from '@ember-data/store';
|
// Importing 'Store' from 'ember-data/store' to get default configuration automatically.
|
||||||
|
// TODO: Remove eslint-disable after upgrading to eslint-plugin-ember >= v12.2.1
|
||||||
|
// eslint-disable-next-line ember/use-ember-data-rfc-395-imports
|
||||||
|
import Store from 'ember-data/store';
|
||||||
|
import { CacheHandler } from '@ember-data/store';
|
||||||
import RequestManager from '@ember-data/request';
|
import RequestManager from '@ember-data/request';
|
||||||
import { LegacyNetworkHandler } from '@ember-data/legacy-compat';
|
import { LegacyNetworkHandler } from '@ember-data/legacy-compat';
|
||||||
import IndexedDbHandler from 'api/handlers/indexed-db-handler';
|
import SqliteHandler from 'api/handlers/sqlite-handler';
|
||||||
|
|
||||||
export default class extends Store {
|
export default class extends Store {
|
||||||
requestManager = new RequestManager();
|
requestManager = new RequestManager();
|
||||||
@@ -14,9 +18,9 @@ export default class extends Store {
|
|||||||
constructor(args) {
|
constructor(args) {
|
||||||
super(args);
|
super(args);
|
||||||
|
|
||||||
const indexedDbHandler = new IndexedDbHandler(this);
|
const sqliteHandler = new SqliteHandler(this);
|
||||||
|
|
||||||
this.requestManager.use([indexedDbHandler, LegacyNetworkHandler]);
|
this.requestManager.use([sqliteHandler, LegacyNetworkHandler]);
|
||||||
this.requestManager.useCache(CacheHandler);
|
this.requestManager.useCache(CacheHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,9 +3,7 @@
|
|||||||
* SPDX-License-Identifier: BUSL-1.1
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Transform from '@ember-data/serializer/transform';
|
export default class AccountValueMapArrayTransform {
|
||||||
|
|
||||||
export default class AccountValueMapArrayTransform extends Transform {
|
|
||||||
deserialize(serialized) {
|
deserialize(serialized) {
|
||||||
const accountValues = serialized || [];
|
const accountValues = serialized || [];
|
||||||
return accountValues.map((accountValueString) => ({
|
return accountValues.map((accountValueString) => ({
|
||||||
@@ -18,4 +16,8 @@ export default class AccountValueMapArrayTransform extends Transform {
|
|||||||
const accountValues = deserialized || [];
|
const accountValues = deserialized || [];
|
||||||
return accountValues.map(({ key, value }) => `${key}=${value}`);
|
return accountValues.map(({ key, value }) => `${key}=${value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static create() {
|
||||||
|
return new this();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,10 +3,9 @@
|
|||||||
* SPDX-License-Identifier: BUSL-1.1
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Transform from '@ember-data/serializer/transform';
|
|
||||||
import { TrackedArray } from 'tracked-built-ins';
|
import { TrackedArray } from 'tracked-built-ins';
|
||||||
|
|
||||||
export default class ArrayTransform extends Transform {
|
export default class ArrayTransform {
|
||||||
deserialize(serialized) {
|
deserialize(serialized) {
|
||||||
const arr = new TrackedArray(serialized || []);
|
const arr = new TrackedArray(serialized || []);
|
||||||
return arr;
|
return arr;
|
||||||
@@ -16,4 +15,8 @@ export default class ArrayTransform extends Transform {
|
|||||||
const arr = deserialized || [];
|
const arr = deserialized || [];
|
||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static create() {
|
||||||
|
return new this();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,9 +3,7 @@
|
|||||||
* SPDX-License-Identifier: BUSL-1.1
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Transform from '@ember-data/serializer/transform';
|
export default class DurationTransform {
|
||||||
|
|
||||||
export default class DurationTransform extends Transform {
|
|
||||||
/**
|
/**
|
||||||
* Normalize a duration string in the format "3.0001s" to milliseconds.
|
* Normalize a duration string in the format "3.0001s" to milliseconds.
|
||||||
* The backend returns a precision of nanoseconds but we round to whole
|
* The backend returns a precision of nanoseconds but we round to whole
|
||||||
@@ -37,4 +35,8 @@ export default class DurationTransform extends Transform {
|
|||||||
maximumFractionDigits: 9,
|
maximumFractionDigits: 9,
|
||||||
})}s`;
|
})}s`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static create() {
|
||||||
|
return new this();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,9 +3,7 @@
|
|||||||
* SPDX-License-Identifier: BUSL-1.1
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Transform from '@ember-data/serializer/transform';
|
export default class HostSourceIdArrayTransform {
|
||||||
|
|
||||||
export default class HostSourceIdArrayTransform extends Transform {
|
|
||||||
deserialize(serialized) {
|
deserialize(serialized) {
|
||||||
const stringValues = serialized || [];
|
const stringValues = serialized || [];
|
||||||
return stringValues.map(({ id: host_source_id, ...obj }) => ({
|
return stringValues.map(({ id: host_source_id, ...obj }) => ({
|
||||||
@@ -18,4 +16,8 @@ export default class HostSourceIdArrayTransform extends Transform {
|
|||||||
const strings = deserialized || [];
|
const strings = deserialized || [];
|
||||||
return strings.map(({ host_source_id: id, ...obj }) => ({ id, ...obj }));
|
return strings.map(({ host_source_id: id, ...obj }) => ({ id, ...obj }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static create() {
|
||||||
|
return new this();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,13 +3,12 @@
|
|||||||
* SPDX-License-Identifier: BUSL-1.1
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Transform from '@ember-data/serializer/transform';
|
export default class ObjectAsArrayTransform {
|
||||||
|
|
||||||
export default class ObjectAsArrayTransform extends Transform {
|
|
||||||
deserialize(serialized) {
|
deserialize(serialized) {
|
||||||
const obj = serialized || {};
|
const obj = serialized || {};
|
||||||
return Object.entries(obj).map(([key, value]) => ({ key, value }));
|
return Object.entries(obj).map(([key, value]) => ({ key, value }));
|
||||||
}
|
}
|
||||||
|
|
||||||
serialize(deserialized) {
|
serialize(deserialized) {
|
||||||
const array = deserialized || [];
|
const array = deserialized || [];
|
||||||
|
|
||||||
@@ -24,4 +23,8 @@ export default class ObjectAsArrayTransform extends Transform {
|
|||||||
return result;
|
return result;
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static create() {
|
||||||
|
return new this();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,10 +3,9 @@
|
|||||||
* SPDX-License-Identifier: BUSL-1.1
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Transform from '@ember-data/serializer/transform';
|
|
||||||
import { TrackedObject } from 'tracked-built-ins';
|
import { TrackedObject } from 'tracked-built-ins';
|
||||||
|
|
||||||
export default class ObjectTransform extends Transform {
|
export default class ObjectTransform {
|
||||||
/**
|
/**
|
||||||
* @param serialized
|
* @param serialized
|
||||||
* @returns {object}
|
* @returns {object}
|
||||||
@@ -26,4 +25,8 @@ export default class ObjectTransform extends Transform {
|
|||||||
}
|
}
|
||||||
return deserialized;
|
return deserialized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static create() {
|
||||||
|
return new this();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,9 +3,7 @@
|
|||||||
* SPDX-License-Identifier: BUSL-1.1
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Transform from '@ember-data/serializer/transform';
|
export default class StringArrayTransform {
|
||||||
|
|
||||||
export default class StringArrayTransform extends Transform {
|
|
||||||
deserialize(serialized) {
|
deserialize(serialized) {
|
||||||
const stringValues = serialized || [];
|
const stringValues = serialized || [];
|
||||||
return stringValues.map((value) => ({ value }));
|
return stringValues.map((value) => ({ value }));
|
||||||
@@ -15,4 +13,8 @@ export default class StringArrayTransform extends Transform {
|
|||||||
const strings = deserialized || [];
|
const strings = deserialized || [];
|
||||||
return strings.map(({ value }) => value);
|
return strings.map(({ value }) => value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static create() {
|
||||||
|
return new this();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
import { typeOf } from '@ember/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flattens a nested object into a single-level object.
|
||||||
|
* @param obj
|
||||||
|
* @param result
|
||||||
|
* @param parentKey
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const flattenObject = (obj, result = {}, parentKey = '') => {
|
||||||
|
for (const key in obj) {
|
||||||
|
// Check if the value is an object and it is not null
|
||||||
|
if (typeOf(obj[key]) === 'object' && obj[key] !== null) {
|
||||||
|
// Recursively flatten the object
|
||||||
|
flattenObject(obj[key], result, `${parentKey}${key}.`);
|
||||||
|
} else if (obj[key] !== null && obj[key] !== '') {
|
||||||
|
// Only add to result if the value is not null or an empty string
|
||||||
|
result[`${parentKey}${key}`] = obj[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) HashiCorp, Inc.
|
|
||||||
* SPDX-License-Identifier: BUSL-1.1
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { get } from '@ember/object';
|
|
||||||
import { modelIndexes } from '../services/indexed-db';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes a POJO representing a filter query and builds a query to
|
|
||||||
* query indexedDb with. Note that the first filter is important as that
|
|
||||||
* will use the actual index in indexedDb while any subsequent ones will be less efficient.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const filter = queryIndexedDb(indexedDb, type, {
|
|
||||||
* search: generated',
|
|
||||||
* filters: {
|
|
||||||
* scope_id: {
|
|
||||||
* logicalOperator: 'and',
|
|
||||||
* values: [{ notEquals: 'scope1' }, { notEquals: 'scope2' }],
|
|
||||||
* },
|
|
||||||
* },
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
export async function queryIndexedDb(indexedDb, resource, query) {
|
|
||||||
let { search, filters = {} } = query ?? {};
|
|
||||||
let filterCollection = indexedDb[resource];
|
|
||||||
|
|
||||||
search = getSearchObject(search, resource);
|
|
||||||
filters = cleanObject(filters);
|
|
||||||
|
|
||||||
if (Object.keys(filters).length) {
|
|
||||||
// Remove the first item to use as the initial collection and then reduce the remaining ones
|
|
||||||
const entries = Object.entries(filters);
|
|
||||||
const [firstKey, firstFilterArrayOrObject] = entries.shift();
|
|
||||||
|
|
||||||
filterCollection = await entries.reduce(
|
|
||||||
async (currCollection, [key, filterArrayOrObject]) => {
|
|
||||||
// Grab the results from the set of one filters to be `AND` together with the rest of the filters
|
|
||||||
const filterResults = await buildInitialWhereClause({
|
|
||||||
table: indexedDb[resource],
|
|
||||||
key,
|
|
||||||
filterArrayOrObject,
|
|
||||||
}).primaryKeys();
|
|
||||||
const resultIdsSet = new Set(filterResults);
|
|
||||||
|
|
||||||
return (await currCollection).and((record) => {
|
|
||||||
return resultIdsSet.has(record.id);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
buildInitialWhereClause({
|
|
||||||
key: firstKey,
|
|
||||||
table: indexedDb[resource],
|
|
||||||
filterArrayOrObject: firstFilterArrayOrObject,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (search) {
|
|
||||||
const { text, fields } = search;
|
|
||||||
|
|
||||||
filterCollection = filterCollection.filter((record) =>
|
|
||||||
fields
|
|
||||||
.map((key) => get(record, key))
|
|
||||||
.some((element) => {
|
|
||||||
if (typeof element === 'string') {
|
|
||||||
return element?.toLowerCase().includes(text?.toLowerCase());
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// OrderBy has to be used on where clauses while if it's a
|
|
||||||
// collection already we have to use sortBy. We'll also sort by
|
|
||||||
// descending order based on created time to match the API as any
|
|
||||||
// "or" clause can change the order
|
|
||||||
if (filterCollection.orderBy) {
|
|
||||||
return filterCollection.orderBy(getKey('created_time')).reverse().toArray();
|
|
||||||
}
|
|
||||||
return filterCollection.reverse().sortBy(getKey('created_time'));
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildInitialWhereClause = ({ filterArrayOrObject, table, key }) => {
|
|
||||||
const filterValueArray = Array.isArray(filterArrayOrObject)
|
|
||||||
? filterArrayOrObject
|
|
||||||
: filterArrayOrObject.values;
|
|
||||||
|
|
||||||
const { logicalOperator } = filterArrayOrObject;
|
|
||||||
|
|
||||||
// Short circuit if all the filter types were equals or notEquals since indexedDb
|
|
||||||
// can more efficiently query if we pass all values at once
|
|
||||||
const checkAllFilterType = (type) =>
|
|
||||||
filterValueArray.every((obj) => Object.keys(obj)[0] === type);
|
|
||||||
if (checkAllFilterType('equals')) {
|
|
||||||
return table
|
|
||||||
.where(getKey(key))
|
|
||||||
.anyOf(filterValueArray.map((obj) => obj['equals']));
|
|
||||||
}
|
|
||||||
if (checkAllFilterType('notEquals')) {
|
|
||||||
return table
|
|
||||||
.where(getKey(key))
|
|
||||||
.noneOf(filterValueArray.map((obj) => obj['notEquals']));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the first item to use as the initial where clause and then reduce the remaining ones
|
|
||||||
const [firstOperation, firstFilterValue] = Object.entries(
|
|
||||||
filterValueArray[0],
|
|
||||||
)[0];
|
|
||||||
|
|
||||||
// Not all are filters are equals or notEquals,
|
|
||||||
// manually build the initial where clause and resulting collections
|
|
||||||
return filterValueArray.slice(1).reduce(
|
|
||||||
(result, currObj) => {
|
|
||||||
const [operation, value] = Object.entries(currObj)[0];
|
|
||||||
|
|
||||||
return buildIndexedDbCollection({
|
|
||||||
key: getKey(key),
|
|
||||||
value,
|
|
||||||
collection: result,
|
|
||||||
operation,
|
|
||||||
logicalOperator,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
buildIndexedDbWhere({
|
|
||||||
table,
|
|
||||||
key: getKey(key),
|
|
||||||
operation: firstOperation,
|
|
||||||
value: firstFilterValue,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds the initial where clause for indexed DB to filter on. Note that this makes
|
|
||||||
* the first filter important as that will do the real filtering from indexed DB while
|
|
||||||
* any subsequent filter has to be done in memory.
|
|
||||||
*/
|
|
||||||
const buildIndexedDbWhere = ({ table, key, value, operation }) => {
|
|
||||||
const baseWhereClause = table.where(key);
|
|
||||||
switch (operation) {
|
|
||||||
case 'contains':
|
|
||||||
// We return an error due to not being able to support contains on first filter due to performance.
|
|
||||||
throw new Error('Contains is not supported as the first filter.');
|
|
||||||
case 'gt':
|
|
||||||
return baseWhereClause.above(value);
|
|
||||||
case 'gte':
|
|
||||||
return baseWhereClause.aboveOrEqual(value);
|
|
||||||
case 'lt':
|
|
||||||
return baseWhereClause.below(value);
|
|
||||||
case 'lte':
|
|
||||||
return baseWhereClause.belowOrEqual(value);
|
|
||||||
case 'notEquals':
|
|
||||||
return baseWhereClause.notEqual(value);
|
|
||||||
default:
|
|
||||||
return baseWhereClause.equals(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Continues building the queries for indexed DB to filter on. Note that these are not true
|
|
||||||
* indexed DB queries and in fact just standard JS filters for logical "and" queries and parallel queries
|
|
||||||
* to indexed DB for logical "or" queries that get deduped.
|
|
||||||
*/
|
|
||||||
const buildIndexedDbCollection = ({
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
collection,
|
|
||||||
operation,
|
|
||||||
logicalOperator,
|
|
||||||
}) => {
|
|
||||||
if (logicalOperator === 'and') {
|
|
||||||
switch (operation) {
|
|
||||||
case 'contains':
|
|
||||||
return collection.and((record) => get(record, key)?.includes(value));
|
|
||||||
case 'gt':
|
|
||||||
return collection.and((record) => get(record, key) > value);
|
|
||||||
case 'gte':
|
|
||||||
return collection.and((record) => get(record, key) >= value);
|
|
||||||
case 'lt':
|
|
||||||
return collection.and((record) => get(record, key) < value);
|
|
||||||
case 'lte':
|
|
||||||
return collection.and((record) => get(record, key) <= value);
|
|
||||||
case 'notEquals':
|
|
||||||
return collection.and((record) => get(record, key) !== value);
|
|
||||||
default:
|
|
||||||
return collection.and((record) => get(record, key) === value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (operation) {
|
|
||||||
case 'contains':
|
|
||||||
// We return an error due to not being able to support contains on this filter due to performance.
|
|
||||||
throw new Error(
|
|
||||||
'Contains is not supported as a filter option with "or" operator.',
|
|
||||||
);
|
|
||||||
case 'gt':
|
|
||||||
return collection.or(key).above(value);
|
|
||||||
case 'gte':
|
|
||||||
return collection.or(key).aboveOrEqual(value);
|
|
||||||
case 'lt':
|
|
||||||
return collection.or(key).below(value);
|
|
||||||
case 'lte':
|
|
||||||
return collection.or(key).belowOrEqual(value);
|
|
||||||
case 'notEquals':
|
|
||||||
return collection.or(key).notEqual(value);
|
|
||||||
default:
|
|
||||||
return collection.or(key).equals(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Translates the fields passed in from the UI for resources to the keys indexed DB knows them as.
|
|
||||||
* Usually it's the path of the actual normalized model.
|
|
||||||
*/
|
|
||||||
const getKey = (key) => {
|
|
||||||
switch (key) {
|
|
||||||
case 'scope_id':
|
|
||||||
return `attributes.scope.${key}`;
|
|
||||||
case 'id':
|
|
||||||
return key;
|
|
||||||
default:
|
|
||||||
return `attributes.${key}`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleans the object to remove any keys that have a value of an empty array. This makes
|
|
||||||
* it easier so we don't have to worry about empty arrays getting queried in indexed db.
|
|
||||||
*/
|
|
||||||
const cleanObject = (obj) => {
|
|
||||||
if (!obj) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.entries(obj).reduce((result, [key, value]) => {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
if (value.length > 0) {
|
|
||||||
result[key] = value;
|
|
||||||
}
|
|
||||||
} else if (value.values.length > 0) {
|
|
||||||
result[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}, {});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes the search object that is expected when a user searches. Uses
|
|
||||||
* the indexes we setup in indexed db.
|
|
||||||
*/
|
|
||||||
const getSearchObject = (text, resource) => {
|
|
||||||
if (!text) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
text,
|
|
||||||
fields: modelIndexes[resource]
|
|
||||||
.split(', ')
|
|
||||||
.map((index) => index.replace('&', '')),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,408 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { modelMapping } from 'api/services/sqlite';
|
||||||
|
import { typeOf } from '@ember/utils';
|
||||||
|
import { underscore } from '@ember/string';
|
||||||
|
import { assert } from '@ember/debug';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a POJO representing a filter query and builds a SQL query.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const filter = generateSQLExpressions(type, {
|
||||||
|
* search: generated',
|
||||||
|
* filters: {
|
||||||
|
* scope_id: {
|
||||||
|
* logicalOperator: 'and',
|
||||||
|
* values: [{ notEquals: 'scope1' }, { notEquals: 'scope2' }],
|
||||||
|
* },
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function generateSQLExpressions(
|
||||||
|
resource,
|
||||||
|
query = {},
|
||||||
|
{ page, pageSize, select } = {},
|
||||||
|
) {
|
||||||
|
let { search, filters, sort = {} } = query;
|
||||||
|
const conditions = [];
|
||||||
|
const parameters = [];
|
||||||
|
const tableName = underscore(resource);
|
||||||
|
const joins = [];
|
||||||
|
|
||||||
|
addFilterConditions({ filters, parameters, conditions, joins, tableName });
|
||||||
|
addSearchConditions({ search, resource, tableName, parameters, conditions });
|
||||||
|
|
||||||
|
const selectClause = constructSelectClause(select, tableName);
|
||||||
|
const joinClause = constructJoinClause(joins);
|
||||||
|
const orderByClause = constructOrderByClause(resource, tableName, sort);
|
||||||
|
const whereClause = conditions.length ? `WHERE ${and(conditions)}` : '';
|
||||||
|
|
||||||
|
const paginationClause = page && pageSize ? `LIMIT ? OFFSET ?` : '';
|
||||||
|
if (paginationClause) {
|
||||||
|
parameters.push(pageSize, (page - 1) * pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Replace any empty newlines or leading whitespace on each line to be consistent with formatting
|
||||||
|
// This is mainly to help us read and test the generated SQL as it has no effect on the actual SQL execution.
|
||||||
|
sql: `
|
||||||
|
${selectClause}
|
||||||
|
${joinClause}
|
||||||
|
${whereClause}
|
||||||
|
${orderByClause}
|
||||||
|
${paginationClause}`
|
||||||
|
.replace(/^\s+/gm, '')
|
||||||
|
.trim(),
|
||||||
|
parameters,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function addFilterConditions({
|
||||||
|
filters,
|
||||||
|
parameters,
|
||||||
|
conditions,
|
||||||
|
joins,
|
||||||
|
tableName,
|
||||||
|
}) {
|
||||||
|
if (!filters) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, filterArrayOrObject] of Object.entries(filters)) {
|
||||||
|
// Filter can be an array or an object with a `values` property
|
||||||
|
const filterValueArray = Array.isArray(filterArrayOrObject)
|
||||||
|
? filterArrayOrObject
|
||||||
|
: filterArrayOrObject.values;
|
||||||
|
|
||||||
|
if (!filterValueArray || !filterValueArray.length || key === 'joins') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When attribute conditions are a series of equals/notEquals
|
||||||
|
// replace with in/notIn to avoid hitting the sqlite maximum
|
||||||
|
// expression tree depth.
|
||||||
|
const firstOperator = Object.keys(filterValueArray[0])[0];
|
||||||
|
const allOperatorsEqual = filterValueArray
|
||||||
|
.flatMap((item) => Object.keys(item))
|
||||||
|
.every((op) => op === firstOperator);
|
||||||
|
if (
|
||||||
|
filterValueArray.length > 1 &&
|
||||||
|
(firstOperator === 'equals' || firstOperator === 'notEquals') &&
|
||||||
|
allOperatorsEqual
|
||||||
|
) {
|
||||||
|
const operation = firstOperator === 'equals' ? 'in' : 'notIn';
|
||||||
|
const nullOperator =
|
||||||
|
firstOperator === 'equals' ? 'IS NULL' : 'IS NOT NULL';
|
||||||
|
const values = filterValueArray
|
||||||
|
.filter(
|
||||||
|
(filterObjValue) =>
|
||||||
|
filterObjValue && Object.values(filterObjValue)[0] !== null,
|
||||||
|
)
|
||||||
|
.map((filterObjValue) => {
|
||||||
|
let value = Object.values(filterObjValue)[0];
|
||||||
|
if (typeOf(value) === 'date') {
|
||||||
|
value = value.toISOString();
|
||||||
|
}
|
||||||
|
parameters.push(value);
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a check for null values as an IN clause will mistakenly use `=` for null values.
|
||||||
|
// We'll add an extra OR clause to handle nulls separately.
|
||||||
|
const isNullValue = filterValueArray.some(
|
||||||
|
(filterObjValue) => Object.values(filterObjValue)[0] === null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const filterCondition = `"${tableName}".${key}${OPERATORS[operation](values)}${isNullValue ? ` OR "${tableName}".${key} ${nullOperator}` : ''}`;
|
||||||
|
conditions.push(parenthetical(filterCondition));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterConditions = filterValueArray
|
||||||
|
.filter((f) => f)
|
||||||
|
.map((filterObjValue) => {
|
||||||
|
let [operation, value] = Object.entries(filterObjValue)[0];
|
||||||
|
|
||||||
|
// Handle null values: convert equals/notEquals to IS NULL or IS NOT NULL
|
||||||
|
if (value === null) {
|
||||||
|
if (operation === 'equals') {
|
||||||
|
return `"${tableName}".${key} IS NULL`;
|
||||||
|
}
|
||||||
|
if (operation === 'notEquals') {
|
||||||
|
return `"${tableName}".${key} IS NOT NULL`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SQLite needs to be working with ISO strings
|
||||||
|
if (typeOf(value) === 'date') {
|
||||||
|
value = value.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle LIKE operator separately
|
||||||
|
if (operation === 'contains') {
|
||||||
|
parameters.push(`%${value}%`);
|
||||||
|
} else {
|
||||||
|
parameters.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `"${tableName}".${key}${OPERATORS[operation]}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { logicalOperator } = filterArrayOrObject;
|
||||||
|
// Default to `or` if we don't have a logical operator
|
||||||
|
conditions.push(
|
||||||
|
parenthetical(
|
||||||
|
logicalOperator === 'and'
|
||||||
|
? and(filterConditions)
|
||||||
|
: or(filterConditions),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.joins?.length > 0) {
|
||||||
|
const tableNameIndex = {};
|
||||||
|
|
||||||
|
filters.joins.forEach((join) => {
|
||||||
|
const {
|
||||||
|
resource,
|
||||||
|
query,
|
||||||
|
joinFrom = 'id',
|
||||||
|
joinOn,
|
||||||
|
joinType = 'INNER',
|
||||||
|
} = join;
|
||||||
|
const joinTableName = underscore(resource);
|
||||||
|
tableNameIndex[resource] ??= 1;
|
||||||
|
// Alias in the possible scenario we join the same table more than once
|
||||||
|
const alias = `${joinTableName}${tableNameIndex[resource]}`;
|
||||||
|
|
||||||
|
joins.push({
|
||||||
|
type: joinType,
|
||||||
|
table: joinTableName,
|
||||||
|
alias,
|
||||||
|
condition: `"${tableName}".${joinFrom} = ${alias}.${joinOn}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add the conditions from the join query
|
||||||
|
if (query?.filters) {
|
||||||
|
addFilterConditions({
|
||||||
|
filters: query.filters,
|
||||||
|
parameters,
|
||||||
|
conditions,
|
||||||
|
tableName: alias,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tableNameIndex[resource]++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSearchConditions({
|
||||||
|
search,
|
||||||
|
resource,
|
||||||
|
tableName,
|
||||||
|
parameters,
|
||||||
|
conditions,
|
||||||
|
}) {
|
||||||
|
if (!search || !modelMapping[resource]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const searchSelect = search?.select ?? 'rowid';
|
||||||
|
const searchSql = `SELECT ${searchSelect} FROM ${tableName}_fts WHERE ${tableName}_fts MATCH ?`;
|
||||||
|
const getParameter = (fields, text) =>
|
||||||
|
fields?.length > 0
|
||||||
|
? or(fields.map((field) => `${field}:"${text}"*`))
|
||||||
|
: `"${text}"*`;
|
||||||
|
|
||||||
|
// Use the special prefix indicator "*" for full-text search
|
||||||
|
if (typeOf(search) === 'object') {
|
||||||
|
if (!search.text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parameter = getParameter(search.fields, search.text);
|
||||||
|
parameters.push(parameter);
|
||||||
|
} else {
|
||||||
|
parameters.push(`"${search}"*`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are extra related searches on other tables, add them too
|
||||||
|
if (search?.relatedSearches?.length > 0) {
|
||||||
|
const relatedSearchQueries = [];
|
||||||
|
|
||||||
|
search.relatedSearches.forEach((relatedSearch) => {
|
||||||
|
const { resource: relatedResource, fields, join } = relatedSearch;
|
||||||
|
const relatedTableName = underscore(relatedResource);
|
||||||
|
|
||||||
|
const parameter = getParameter(fields, search.text);
|
||||||
|
parameters.push(parameter);
|
||||||
|
|
||||||
|
// Build the related FTS query with optional join
|
||||||
|
let relatedQuery = [];
|
||||||
|
relatedQuery.push(
|
||||||
|
`SELECT "${tableName}".${searchSelect} FROM ${relatedTableName}_fts`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (join) {
|
||||||
|
const { joinFrom = 'id', joinOn } = join;
|
||||||
|
relatedQuery.push(
|
||||||
|
`JOIN "${tableName}" ON "${tableName}".${joinFrom} = ${relatedResource}_fts.${joinOn}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
relatedQuery.push(`WHERE ${relatedTableName}_fts MATCH ?`);
|
||||||
|
|
||||||
|
relatedSearchQueries.push(relatedQuery.join(' '));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add the original search first since we've already added the original parameter first
|
||||||
|
const conditionStatement = [searchSql, ...relatedSearchQueries].join(
|
||||||
|
'\nUNION ',
|
||||||
|
);
|
||||||
|
conditions.push(
|
||||||
|
`"${tableName}".${searchSelect} IN (${conditionStatement})`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a subquery to match against the FTS table with rowids as SQLite is
|
||||||
|
// much more efficient with FTS queries when using rowids or MATCH (or both).
|
||||||
|
// We could have also used a join here but a subquery is simpler.
|
||||||
|
conditions.push(`"${tableName}".${searchSelect} IN (${searchSql})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function constructSelectClause(select = [{ field: '*' }], tableName) {
|
||||||
|
const distinctColumns = select.filter(({ isDistinct }) => isDistinct);
|
||||||
|
const nonDistinctColumns = select.filter(({ isDistinct }) => !isDistinct);
|
||||||
|
|
||||||
|
const buildColumnExpression = ({ field, isCount, alias, isDistinct }) => {
|
||||||
|
let column = field === '*' ? field : `"${tableName}".${field}`;
|
||||||
|
|
||||||
|
// If there's just distinct with nothing else, this will be handled separately
|
||||||
|
// and we will just return the column back as is
|
||||||
|
if (isCount && isDistinct) {
|
||||||
|
column = `count(DISTINCT ${column})`;
|
||||||
|
} else if (isCount) {
|
||||||
|
column = `count(${column})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alias) {
|
||||||
|
column = `${column} as ${alias}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return column;
|
||||||
|
};
|
||||||
|
|
||||||
|
let selectColumns;
|
||||||
|
|
||||||
|
if (distinctColumns.length > 0) {
|
||||||
|
// Check if any columns also have COUNT (or other aggregate function in the future)
|
||||||
|
const hasAggregateDistinct = distinctColumns.some((col) => col.isCount);
|
||||||
|
|
||||||
|
if (hasAggregateDistinct) {
|
||||||
|
selectColumns = distinctColumns.map(buildColumnExpression).join(', ');
|
||||||
|
|
||||||
|
// Add back in the non distinct columns
|
||||||
|
if (nonDistinctColumns.length > 0) {
|
||||||
|
const regularPart = nonDistinctColumns
|
||||||
|
.map(buildColumnExpression)
|
||||||
|
.join(', ');
|
||||||
|
selectColumns = `${selectColumns}, ${regularPart}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert(
|
||||||
|
'Can not combine non-distincts with multi column distincts',
|
||||||
|
nonDistinctColumns.length === 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only do multi column DISTINCT with no aggregate functions
|
||||||
|
selectColumns = `DISTINCT ${distinctColumns.map(buildColumnExpression).join(', ')}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectColumns = select.map(buildColumnExpression).join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
return `SELECT ${selectColumns} FROM "${tableName}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function constructJoinClause(joins) {
|
||||||
|
if (!joins || joins.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return joins
|
||||||
|
.map(
|
||||||
|
({ type, table, alias, condition }) =>
|
||||||
|
`${type} JOIN "${table}" ${alias} ON ${condition}`,
|
||||||
|
)
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function constructOrderByClause(resource, tableName, sort) {
|
||||||
|
const defaultOrderByClause = `ORDER BY "${tableName}".created_time DESC`;
|
||||||
|
|
||||||
|
const { attributes, customSort, direction, isCoalesced } = sort;
|
||||||
|
const sortDirection = direction === 'desc' ? 'DESC' : 'ASC';
|
||||||
|
const attributesWithTableName = attributes?.map(
|
||||||
|
(attribute) => `"${tableName}".${attribute}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// We have to check if the attributes are valid for the resource
|
||||||
|
// as we can't use parameterized queries for ORDER BY
|
||||||
|
const validAttributes = Object.keys(modelMapping[resource] ?? {});
|
||||||
|
if (attributes?.some((attr) => !validAttributes.includes(attr))) {
|
||||||
|
return modelMapping[resource]?.created_time ? defaultOrderByClause : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customSort?.attributeMap) {
|
||||||
|
const whenClauses = Object.keys(customSort.attributeMap).reduce(
|
||||||
|
(acc, key) => {
|
||||||
|
return acc + `WHEN '${key}' THEN '${customSort.attributeMap[key]}' `;
|
||||||
|
},
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
return `ORDER BY CASE ${attributesWithTableName.join(', ')} ${whenClauses}END ${sortDirection}`;
|
||||||
|
} else if (attributes?.length > 0) {
|
||||||
|
const commaSeparatedVals = attributesWithTableName.join(', ');
|
||||||
|
|
||||||
|
// In places where `collate nocase` is used, it is to ensure case is ignored on the initial sort.
|
||||||
|
// Then, a sort on the same condition is performed to ensure upper-case strings are given preference in a tie.
|
||||||
|
if (isCoalesced) {
|
||||||
|
return `ORDER BY COALESCE(${commaSeparatedVals}) COLLATE NOCASE ${sortDirection}, COALESCE(${commaSeparatedVals}) ${sortDirection}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attributesWithNoCollate = attributes
|
||||||
|
.map((attr) => `"${tableName}".${attr} COLLATE NOCASE ${sortDirection}`)
|
||||||
|
.join(', ');
|
||||||
|
const attributesWithDirection = attributes
|
||||||
|
.map((attr) => `"${tableName}".${attr} ${sortDirection}`)
|
||||||
|
.join(', ');
|
||||||
|
return `ORDER BY ${attributesWithNoCollate}, ${attributesWithDirection}`;
|
||||||
|
} else if (modelMapping[resource]?.created_time) {
|
||||||
|
return defaultOrderByClause;
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comparison Operators
|
||||||
|
const OPERATORS = {
|
||||||
|
equals: ' = ?',
|
||||||
|
notEquals: ' != ?',
|
||||||
|
gt: ' > ?',
|
||||||
|
gte: ' >= ?',
|
||||||
|
lt: ' < ?',
|
||||||
|
lte: ' <= ?',
|
||||||
|
contains: ' LIKE ?',
|
||||||
|
in: (values) => ` IN (${values.map(() => '?').join(', ')})`,
|
||||||
|
notIn: (values) => ` NOT IN (${values.map(() => '?').join(', ')})`,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Logical Operators
|
||||||
|
const and = (clauses) => clauses.join(' AND ');
|
||||||
|
const or = (clauses) => clauses.join(' OR ');
|
||||||
|
const parenthetical = (expression) => (expression ? `(${expression})` : '');
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { PWBWorker } from 'promise-worker-bi';
|
||||||
|
import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
|
||||||
|
import sharedServiceInit from './utils/shared-service';
|
||||||
|
import {
|
||||||
|
CLEAR_DB,
|
||||||
|
CREATE_TABLES,
|
||||||
|
DELETE_STATEMENT,
|
||||||
|
INSERT_STATEMENTS,
|
||||||
|
} from './utils/schema';
|
||||||
|
|
||||||
|
// Production builds error out when using top level awaits so we'll just use an async IIFE
|
||||||
|
(async () => {
|
||||||
|
/** @type {import('@sqlite.org/sqlite-wasm').OpfsSAHPoolDatabase} */
|
||||||
|
let db;
|
||||||
|
/** @type {import('@sqlite.org/sqlite-wasm').SAHPoolUtil} */
|
||||||
|
let poolUtil;
|
||||||
|
/** @type {import('@sqlite.org/sqlite-wasm').Sqlite3Static} */
|
||||||
|
let sqlite3;
|
||||||
|
|
||||||
|
const { promise: ready, resolve: readyResolve } = Promise.withResolvers();
|
||||||
|
|
||||||
|
// Maximum number of host parameters for SQLite prepared statements.
|
||||||
|
// See "Maximum Number Of Host Parameters In A Single SQL Statement" in
|
||||||
|
// https://www.sqlite.org/limits.html
|
||||||
|
const MAX_HOST_PARAMETERS = 32766;
|
||||||
|
const SCHEMA_VERSION = 2;
|
||||||
|
|
||||||
|
// Some browsers do not allow calling getDirectory in private browsing modes even
|
||||||
|
// if we're in a secure context. This will cause the SQLite setup to fail so we should
|
||||||
|
// check ahead of time to see if we're able to call the API.
|
||||||
|
let canCallGetDirectory;
|
||||||
|
try {
|
||||||
|
canCallGetDirectory = Boolean(await navigator.storage.getDirectory());
|
||||||
|
} catch {
|
||||||
|
canCallGetDirectory = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSecure = self.isSecureContext && canCallGetDirectory;
|
||||||
|
|
||||||
|
// Error code for sqlite corruption errors.
|
||||||
|
// See https://sqlite.org/rescode.html#corrupt
|
||||||
|
const SQLITE_CORRUPT = 11;
|
||||||
|
|
||||||
|
const methods = {
|
||||||
|
initializeSQLite: async () => {
|
||||||
|
sqlite3 = await sqlite3InitModule({
|
||||||
|
print: console.log,
|
||||||
|
printErr: console.error,
|
||||||
|
});
|
||||||
|
const dbName = await promiseWorker.postMessage({
|
||||||
|
method: 'getDatabaseName',
|
||||||
|
});
|
||||||
|
|
||||||
|
function setupDb() {
|
||||||
|
const [row] = db.exec('PRAGMA user_version;', { rowMode: 'object' });
|
||||||
|
// Check if we're on a newer schema version and clear the database if so
|
||||||
|
if (SCHEMA_VERSION > row.user_version) {
|
||||||
|
db.exec(CLEAR_DB);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.exec(CREATE_TABLES(SCHEMA_VERSION));
|
||||||
|
// Force an analyze on initial connection
|
||||||
|
db.exec('PRAGMA optimize=0x10002;');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instantiate an in memory DB if we're not in a secure context as OPFS is unavailable otherwise
|
||||||
|
if (!isSecure) {
|
||||||
|
db = new sqlite3.oo1.DB(':memory:');
|
||||||
|
setupDb();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let done = false;
|
||||||
|
while (!done) {
|
||||||
|
try {
|
||||||
|
poolUtil = await sqlite3.installOpfsSAHPoolVfs();
|
||||||
|
db = new poolUtil.OpfsSAHPoolDb(dbName);
|
||||||
|
|
||||||
|
setupDb();
|
||||||
|
done = true;
|
||||||
|
} catch (/** @type {import('@sqlite.org/sqlite-wasm').SQLite3Error} */ err) {
|
||||||
|
// If we get a SQLite3Error and the pool is at capacity, we'll assume the error is
|
||||||
|
// because we hit the cap so we add more capacity and see if that solves it.
|
||||||
|
if (
|
||||||
|
err.name === 'SQLite3Error' &&
|
||||||
|
poolUtil.getCapacity() === poolUtil.getFileCount()
|
||||||
|
) {
|
||||||
|
await poolUtil.addCapacity(1);
|
||||||
|
|
||||||
|
// If we have more than 20 files, that might indicate a problem so let's just
|
||||||
|
// remove the VFS and wipe all the client data across users and restart.
|
||||||
|
if (poolUtil.getCapacity() > 20) {
|
||||||
|
await poolUtil.removeVfs();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('SQLite initialization error:', err);
|
||||||
|
|
||||||
|
// We might have gotten ourselves into a bad state which corrupted the DB so clear it out and retry
|
||||||
|
if (err?.resultCode === SQLITE_CORRUPT) {
|
||||||
|
poolUtil.unlink(`/${dbName}`);
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
analyzeDatabase: () => {
|
||||||
|
db.exec('PRAGMA optimize');
|
||||||
|
},
|
||||||
|
clearDatabase: () => {
|
||||||
|
db.exec(CLEAR_DB);
|
||||||
|
db.exec(CREATE_TABLES(SCHEMA_VERSION));
|
||||||
|
},
|
||||||
|
deleteDatabase: () => {
|
||||||
|
if (!db) {
|
||||||
|
throw new Error('No database was initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = db.dbFilename();
|
||||||
|
db.close();
|
||||||
|
db = null;
|
||||||
|
poolUtil.unlink(`/${name}`);
|
||||||
|
},
|
||||||
|
downloadDatabase: () => {
|
||||||
|
return sqlite3.capi.sqlite3_js_db_export(db);
|
||||||
|
},
|
||||||
|
fetchResource: ({ sql, parameters = [] }) => {
|
||||||
|
try {
|
||||||
|
return db.exec({
|
||||||
|
sql,
|
||||||
|
bind: parameters,
|
||||||
|
rowMode: 'object',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
// TODO: Add error handling to rest of methods and properly return the error back to caller
|
||||||
|
console.error('Fetch resource error:', err, sql, parameters);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
insertResource: ({ resource, items, modelMapping }) => {
|
||||||
|
if (items.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const numberOfParameters = items.reduce((acc, item) => {
|
||||||
|
return acc + item.length;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// We want to use as many VALUES as part of the insert statement as it's quicker
|
||||||
|
// than doing multiple inserts. If we have too many rows to insert, we'll manually
|
||||||
|
// chunk them up to avoid hitting the maximum number of host parameters
|
||||||
|
// and execute these separate insert statements in a transaction
|
||||||
|
if (numberOfParameters > MAX_HOST_PARAMETERS) {
|
||||||
|
const chunkSize = Math.floor(MAX_HOST_PARAMETERS / items[0].length);
|
||||||
|
|
||||||
|
return db.transaction(() => {
|
||||||
|
const results = [];
|
||||||
|
for (let i = 0; i < items.length; i += chunkSize) {
|
||||||
|
const chunk = items.slice(i, i + chunkSize);
|
||||||
|
const result = db.exec({
|
||||||
|
sql: INSERT_STATEMENTS(resource, chunk, modelMapping),
|
||||||
|
bind: chunk.flat(),
|
||||||
|
rowMode: 'object',
|
||||||
|
});
|
||||||
|
results.push(result);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
db.exec({
|
||||||
|
sql: INSERT_STATEMENTS(resource, items, modelMapping),
|
||||||
|
bind: items.flat(),
|
||||||
|
rowMode: 'object',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteResource: ({ resource, ids }) => {
|
||||||
|
// Check if we have too many parameters for the deletion and chunk if so
|
||||||
|
if (ids?.length > MAX_HOST_PARAMETERS) {
|
||||||
|
return db.transaction(() => {
|
||||||
|
const results = [];
|
||||||
|
for (let i = 0; i < ids.length; i += MAX_HOST_PARAMETERS) {
|
||||||
|
const chunk = ids.slice(i, i + MAX_HOST_PARAMETERS);
|
||||||
|
const result = db.exec({
|
||||||
|
sql: DELETE_STATEMENT(resource, chunk),
|
||||||
|
bind: chunk,
|
||||||
|
rowMode: 'object',
|
||||||
|
});
|
||||||
|
results.push(result);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.exec({
|
||||||
|
sql: DELETE_STATEMENT(resource, ids),
|
||||||
|
bind: ids,
|
||||||
|
rowMode: 'object',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const promiseWorker = new PWBWorker();
|
||||||
|
promiseWorker.register(async ({ method, payload }) => {
|
||||||
|
if (!sharedService) {
|
||||||
|
await ready;
|
||||||
|
return methods[method](payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
await sharedService.status.ready;
|
||||||
|
return sharedService.proxy[method](payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
const initializeOnProviderChange = async (isProvider) => {
|
||||||
|
if (!isProvider) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Initialize the SQLite database when we become a provider
|
||||||
|
await sharedService.proxy.initializeSQLite();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only use the shared service in a secure context as weblocks aren't available otherwise
|
||||||
|
let sharedService;
|
||||||
|
if (isSecure) {
|
||||||
|
sharedService = sharedServiceInit(
|
||||||
|
'sqlite',
|
||||||
|
methods,
|
||||||
|
initializeOnProviderChange,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await methods.initializeSQLite();
|
||||||
|
readyResolve();
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -0,0 +1,480 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { underscore } from '@ember/string';
|
||||||
|
|
||||||
|
export const CLEAR_DB = `
|
||||||
|
PRAGMA writable_schema = 1;
|
||||||
|
DELETE FROM sqlite_master;
|
||||||
|
PRAGMA writable_schema = 0;
|
||||||
|
VACUUM;
|
||||||
|
PRAGMA integrity_check;`;
|
||||||
|
|
||||||
|
const createTargetTables = `
|
||||||
|
CREATE TABLE IF NOT EXISTS target (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
address TEXT,
|
||||||
|
scope_id TEXT NOT NULL,
|
||||||
|
created_time TEXT NOT NULL,
|
||||||
|
data TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_target_scope_id_created_time ON target(scope_id, created_time DESC);
|
||||||
|
|
||||||
|
-- Create a content FTS table that references the original table
|
||||||
|
CREATE VIRTUAL TABLE IF NOT EXISTS target_fts USING fts5(
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
address,
|
||||||
|
scope_id,
|
||||||
|
created_time,
|
||||||
|
content='target',
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create triggers to keep the FTS table in sync with the target table
|
||||||
|
-- Note: The FTS table does not have an update trigger, as it's updated only on
|
||||||
|
-- INSERT and DELETE as we do an INSERT OR REPLACE INTO on the table
|
||||||
|
-- which replaces the row and doesn't execute an UPDATE trigger.
|
||||||
|
CREATE TRIGGER IF NOT EXISTS target_ai AFTER INSERT ON target BEGIN
|
||||||
|
INSERT INTO target_fts(
|
||||||
|
rowid, id, type, name, description, address, scope_id, created_time
|
||||||
|
) VALUES (
|
||||||
|
new.rowid, new.id, new.type, new.name, new.description, new.address, new.scope_id, new.created_time
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS target_ad AFTER DELETE ON target BEGIN
|
||||||
|
INSERT INTO target_fts(target_fts, rowid, id, type, name, description, address, scope_id, created_time)
|
||||||
|
VALUES('delete', old.rowid, old.id, old.type, old.name, old.description, old.address, old.scope_id, old.created_time);
|
||||||
|
END;`;
|
||||||
|
|
||||||
|
const createAliasTables = `
|
||||||
|
CREATE TABLE IF NOT EXISTS alias (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
name TEXT,
|
||||||
|
description TEXT,
|
||||||
|
destination_id TEXT,
|
||||||
|
value TEXT,
|
||||||
|
scope_id TEXT NOT NULL,
|
||||||
|
created_time TEXT NOT NULL,
|
||||||
|
data TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_alias_created_time ON alias(created_time DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_alias_destination_id ON alias(destination_id);
|
||||||
|
|
||||||
|
CREATE VIRTUAL TABLE IF NOT EXISTS alias_fts USING fts5(
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
destination_id,
|
||||||
|
value,
|
||||||
|
scope_id,
|
||||||
|
created_time,
|
||||||
|
content='alias',
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS alias_ai AFTER INSERT ON alias BEGIN
|
||||||
|
INSERT INTO alias_fts(
|
||||||
|
rowid, id, type, name, description, destination_id, value, scope_id, created_time
|
||||||
|
) VALUES (
|
||||||
|
new.rowid, new.id, new.type, new.name, new.description, new.destination_id, new.value, new.scope_id, new.created_time
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS alias_ad AFTER DELETE ON alias BEGIN
|
||||||
|
INSERT INTO alias_fts(alias_fts, rowid, id, type, name, description, destination_id, value, scope_id, created_time)
|
||||||
|
VALUES('delete', old.rowid, old.id, old.type, old.name, old.description, old.destination_id, old.value, old.scope_id, old.created_time);
|
||||||
|
END;`;
|
||||||
|
|
||||||
|
const createGroupTables = `
|
||||||
|
CREATE TABLE IF NOT EXISTS "group" (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
name TEXT,
|
||||||
|
description TEXT,
|
||||||
|
scope_id TEXT NOT NULL,
|
||||||
|
created_time TEXT NOT NULL,
|
||||||
|
data TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_group_scope_id_created_time ON "group"(scope_id, created_time DESC);
|
||||||
|
|
||||||
|
CREATE VIRTUAL TABLE IF NOT EXISTS group_fts USING fts5(
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
scope_id,
|
||||||
|
created_time,
|
||||||
|
content='group',
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS group_ai AFTER INSERT ON "group" BEGIN
|
||||||
|
INSERT INTO group_fts(
|
||||||
|
rowid, id, name, description, scope_id, created_time
|
||||||
|
) VALUES (
|
||||||
|
new.rowid, new.id, new.name, new.description, new.scope_id, new.created_time
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS group_ad AFTER DELETE ON "group" BEGIN
|
||||||
|
INSERT INTO group_fts(group_fts, rowid, id, name, description, scope_id, created_time)
|
||||||
|
VALUES('delete', old.rowid, old.id, old.name, old.description, old.scope_id, old.created_time);
|
||||||
|
END;`;
|
||||||
|
|
||||||
|
const createRoleTables = `
|
||||||
|
CREATE TABLE IF NOT EXISTS role (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
name TEXT,
|
||||||
|
description TEXT,
|
||||||
|
scope_id TEXT NOT NULL,
|
||||||
|
created_time TEXT NOT NULL,
|
||||||
|
data TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_role_scope_id_created_time ON role(scope_id, created_time DESC);
|
||||||
|
|
||||||
|
CREATE VIRTUAL TABLE IF NOT EXISTS role_fts USING fts5(
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
scope_id,
|
||||||
|
created_time,
|
||||||
|
content='role',
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS role_ai AFTER INSERT ON role BEGIN
|
||||||
|
INSERT INTO role_fts(
|
||||||
|
rowid, id, name, description, scope_id, created_time
|
||||||
|
) VALUES (
|
||||||
|
new.rowid, new.id, new.name, new.description, new.scope_id, new.created_time
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS role_ad AFTER DELETE ON role BEGIN
|
||||||
|
INSERT INTO role_fts(role_fts, rowid, id, name, description, scope_id, created_time)
|
||||||
|
VALUES('delete', old.rowid, old.id, old.name, old.description, old.scope_id, old.created_time);
|
||||||
|
END;`;
|
||||||
|
|
||||||
|
const createUserTables = `
|
||||||
|
CREATE TABLE IF NOT EXISTS user (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
name TEXT,
|
||||||
|
description TEXT,
|
||||||
|
scope_id TEXT NOT NULL,
|
||||||
|
created_time TEXT NOT NULL,
|
||||||
|
data TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_scope_id_created_time ON user(scope_id, created_time DESC);
|
||||||
|
|
||||||
|
CREATE VIRTUAL TABLE IF NOT EXISTS user_fts USING fts5(
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
scope_id,
|
||||||
|
created_time,
|
||||||
|
content='user',
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS user_ai AFTER INSERT ON user BEGIN
|
||||||
|
INSERT INTO user_fts(
|
||||||
|
rowid, id, name, description, scope_id, created_time
|
||||||
|
) VALUES (
|
||||||
|
new.rowid, new.id, new.name, new.description, new.scope_id, new.created_time
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS user_ad AFTER DELETE ON user BEGIN
|
||||||
|
INSERT INTO user_fts(user_fts, rowid, id, name, description, scope_id, created_time)
|
||||||
|
VALUES('delete', old.rowid, old.id, old.name, old.description, old.scope_id, old.created_time);
|
||||||
|
END;`;
|
||||||
|
|
||||||
|
const createCredentialStoreTables = `
|
||||||
|
CREATE TABLE IF NOT EXISTS credential_store (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
name TEXT,
|
||||||
|
description TEXT,
|
||||||
|
scope_id TEXT NOT NULL,
|
||||||
|
created_time TEXT NOT NULL,
|
||||||
|
data TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_credential_store_scope_id_created_time ON credential_store(scope_id, created_time DESC);
|
||||||
|
|
||||||
|
CREATE VIRTUAL TABLE IF NOT EXISTS credential_store_fts USING fts5(
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
scope_id,
|
||||||
|
created_time,
|
||||||
|
content='credential_store',
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS credential_store_ai AFTER INSERT ON credential_store BEGIN
|
||||||
|
INSERT INTO credential_store_fts(
|
||||||
|
rowid, id, type, name, description, scope_id, created_time
|
||||||
|
) VALUES (
|
||||||
|
new.rowid, new.id, new.type, new.name, new.description, new.scope_id, new.created_time
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS credential_store_ad AFTER DELETE ON credential_store BEGIN
|
||||||
|
INSERT INTO credential_store_fts(credential_store_fts, rowid, id, type, name, description, scope_id, created_time)
|
||||||
|
VALUES('delete', old.rowid, old.id, old.type, old.name, old.description, old.scope_id, old.created_time);
|
||||||
|
END;`;
|
||||||
|
|
||||||
|
const createScopeTables = `
|
||||||
|
CREATE TABLE IF NOT EXISTS scope (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
name TEXT,
|
||||||
|
description TEXT,
|
||||||
|
scope_id TEXT NOT NULL,
|
||||||
|
created_time TEXT NOT NULL,
|
||||||
|
data TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_scope_scope_id_created_time ON scope(scope_id, created_time DESC);
|
||||||
|
|
||||||
|
CREATE VIRTUAL TABLE IF NOT EXISTS scope_fts USING fts5(
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
scope_id,
|
||||||
|
created_time,
|
||||||
|
content='scope',
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS scope_ai AFTER INSERT ON scope BEGIN
|
||||||
|
INSERT INTO scope_fts(
|
||||||
|
rowid, id, type, name, description, scope_id, created_time
|
||||||
|
) VALUES (
|
||||||
|
new.rowid, new.id, new.type, new.name, new.description, new.scope_id, new.created_time
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS scope_ad AFTER DELETE ON scope BEGIN
|
||||||
|
INSERT INTO scope_fts(scope_fts, rowid, id, type, name, description, scope_id, created_time)
|
||||||
|
VALUES('delete', old.rowid, old.id, old.type, old.name, old.description, old.scope_id, old.created_time);
|
||||||
|
END;`;
|
||||||
|
|
||||||
|
const createAuthMethodTables = `
|
||||||
|
CREATE TABLE IF NOT EXISTS auth_method (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
name TEXT,
|
||||||
|
description TEXT,
|
||||||
|
is_primary INT,
|
||||||
|
scope_id TEXT NOT NULL,
|
||||||
|
created_time TEXT NOT NULL,
|
||||||
|
data TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_auth_method_scope_id_created_time ON auth_method(scope_id, created_time DESC);
|
||||||
|
|
||||||
|
CREATE VIRTUAL TABLE IF NOT EXISTS auth_method_fts USING fts5(
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
is_primary,
|
||||||
|
scope_id,
|
||||||
|
created_time,
|
||||||
|
content='auth_method',
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS auth_method_ai AFTER INSERT ON auth_method BEGIN
|
||||||
|
INSERT INTO auth_method_fts(
|
||||||
|
rowid, id, type, name, description, is_primary, scope_id, created_time
|
||||||
|
) VALUES (
|
||||||
|
new.rowid, new.id, new.type, new.name, new.description, new.is_primary, new.scope_id, new.created_time
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS auth_method_ad AFTER DELETE ON auth_method BEGIN
|
||||||
|
INSERT INTO auth_method_fts(auth_method_fts, rowid, id, type, name, description, is_primary, scope_id, created_time)
|
||||||
|
VALUES('delete', old.rowid, old.id, old.type, old.name, old.description, old.is_primary, old.scope_id, old.created_time);
|
||||||
|
END;`;
|
||||||
|
|
||||||
|
const createHostCatalogTables = `
|
||||||
|
CREATE TABLE IF NOT EXISTS host_catalog (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
name TEXT,
|
||||||
|
description TEXT,
|
||||||
|
plugin_name TEXT,
|
||||||
|
scope_id TEXT NOT NULL,
|
||||||
|
created_time TEXT NOT NULL,
|
||||||
|
data TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_host_catalog_scope_id_created_time ON host_catalog(scope_id, created_time DESC);
|
||||||
|
|
||||||
|
CREATE VIRTUAL TABLE IF NOT EXISTS host_catalog_fts USING fts5(
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
plugin_name,
|
||||||
|
scope_id,
|
||||||
|
created_time,
|
||||||
|
content='host_catalog',
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS host_catalog_ai AFTER INSERT ON host_catalog BEGIN
|
||||||
|
INSERT INTO host_catalog_fts(
|
||||||
|
rowid, id, type, name, description, plugin_name, scope_id, created_time
|
||||||
|
) VALUES (
|
||||||
|
new.rowid, new.id, new.type, new.name, new.description, new.plugin_name, new.scope_id, new.created_time
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS host_catalog_ad AFTER DELETE ON host_catalog BEGIN
|
||||||
|
INSERT INTO host_catalog_fts(host_catalog_fts, rowid, id, type, name, description, plugin_name, scope_id, created_time)
|
||||||
|
VALUES('delete', old.rowid, old.id, old.type, old.name, old.description, old.plugin_name, old.scope_id, old.created_time);
|
||||||
|
END;`;
|
||||||
|
|
||||||
|
const createSessionRecordingTables = `
|
||||||
|
CREATE TABLE IF NOT EXISTS session_recording (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
state TEXT,
|
||||||
|
start_time TEXT,
|
||||||
|
end_time TEXT,
|
||||||
|
duration TEXT,
|
||||||
|
scope_id TEXT NOT NULL,
|
||||||
|
user_id TEXT,
|
||||||
|
user_name TEXT,
|
||||||
|
target_id TEXT,
|
||||||
|
target_name TEXT,
|
||||||
|
target_scope_id TEXT,
|
||||||
|
target_scope_name TEXT,
|
||||||
|
target_scope_parent_scope_id TEXT,
|
||||||
|
created_time TEXT NOT NULL,
|
||||||
|
data TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_session_recording_created_time ON session_recording(created_time DESC);
|
||||||
|
|
||||||
|
CREATE VIRTUAL TABLE IF NOT EXISTS session_recording_fts USING fts5(
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
state,
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
duration,
|
||||||
|
scope_id,
|
||||||
|
user_id,
|
||||||
|
user_name,
|
||||||
|
target_id,
|
||||||
|
target_name,
|
||||||
|
target_scope_id,
|
||||||
|
target_scope_name,
|
||||||
|
target_scope_parent_scope_id,
|
||||||
|
created_time,
|
||||||
|
content='session_recording',
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS session_recording_ai AFTER INSERT ON session_recording BEGIN
|
||||||
|
INSERT INTO session_recording_fts(
|
||||||
|
rowid, id, type, state, start_time, end_time, duration, scope_id, user_id, user_name, target_id, target_name, target_scope_id, target_scope_name, target_scope_parent_scope_id, created_time
|
||||||
|
) VALUES (
|
||||||
|
new.rowid, new.id, new.type, new.state, new.start_time, new.end_time, new.duration, new.scope_id, new.user_id, new.user_name, new.target_id, new.target_name, new.target_scope_id, new.target_scope_name, new.target_scope_parent_scope_id, new.created_time
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS session_recording_ad AFTER DELETE ON session_recording BEGIN
|
||||||
|
INSERT INTO session_recording_fts(session_recording_fts, rowid, id, type, state, start_time, end_time, duration, scope_id, user_id, user_name, target_id, target_name, target_scope_id, target_scope_name, target_scope_parent_scope_id, created_time)
|
||||||
|
VALUES('delete', old.rowid, old.id, old.type, old.state, old.start_time, old.end_time, old.duration, old.scope_id, old.user_id, old.user_name, old.target_id, old.target_name, old.target_scope_id, old.target_scope_name, old.target_scope_parent_scope_id, old.created_time);
|
||||||
|
END;`;
|
||||||
|
|
||||||
|
const createSessionTables = `
|
||||||
|
CREATE TABLE IF NOT EXISTS session (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
type TEXT,
|
||||||
|
status TEXT,
|
||||||
|
endpoint TEXT,
|
||||||
|
target_id TEXT,
|
||||||
|
user_id TEXT,
|
||||||
|
scope_id TEXT NOT NULL,
|
||||||
|
created_time TEXT NOT NULL,
|
||||||
|
data TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_session_scope_id_created_time ON session(scope_id, created_time DESC);
|
||||||
|
|
||||||
|
CREATE VIRTUAL TABLE IF NOT EXISTS session_fts USING fts5(
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
status,
|
||||||
|
endpoint,
|
||||||
|
target_id,
|
||||||
|
user_id,
|
||||||
|
scope_id,
|
||||||
|
created_time,
|
||||||
|
content='session',
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS session_ai AFTER INSERT ON session BEGIN
|
||||||
|
INSERT INTO session_fts(
|
||||||
|
rowid, id, type, status, endpoint, target_id, user_id, scope_id, created_time
|
||||||
|
) VALUES (
|
||||||
|
new.rowid, new.id, new.type, new.status, new.endpoint, new.target_id, new.user_id, new.scope_id, new.created_time
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS session_ad AFTER DELETE ON session BEGIN
|
||||||
|
INSERT INTO session_fts(session_fts, rowid, id, type, status, endpoint, target_id, user_id, scope_id, created_time)
|
||||||
|
VALUES('delete', old.rowid, old.id, old.type, old.status, old.endpoint, old.target_id, old.user_id, old.scope_id, old.created_time);
|
||||||
|
END;`;
|
||||||
|
|
||||||
|
export const CREATE_TABLES = (version) => `
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- This ensures multiple triggers can activate in the scenario of using REPLACE INTO
|
||||||
|
PRAGMA recursive_triggers = true;
|
||||||
|
PRAGMA user_version = ${version};
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS token (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
token TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
${createTargetTables}
|
||||||
|
${createAliasTables}
|
||||||
|
${createGroupTables}
|
||||||
|
${createRoleTables}
|
||||||
|
${createUserTables}
|
||||||
|
${createCredentialStoreTables}
|
||||||
|
${createScopeTables}
|
||||||
|
${createAuthMethodTables}
|
||||||
|
${createHostCatalogTables}
|
||||||
|
${createSessionRecordingTables}
|
||||||
|
${createSessionTables}
|
||||||
|
|
||||||
|
COMMIT;`;
|
||||||
|
|
||||||
|
export const INSERT_STATEMENTS = (resource, items, modelMapping) => {
|
||||||
|
if (resource === 'token') {
|
||||||
|
return `REPLACE INTO token (id, token) VALUES ${items.map(() => '(?, ?)').join(', ')};`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!modelMapping?.[resource]) {
|
||||||
|
throw new Error(`modelMapping is required for ${resource} insertions.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = Object.keys(modelMapping[resource]);
|
||||||
|
// Extra column for the data column
|
||||||
|
const numColumns = Array(columns.length + 1);
|
||||||
|
const placeholders = `(${numColumns.fill('?').join(', ')})`;
|
||||||
|
|
||||||
|
return `REPLACE INTO "${underscore(resource)}" (${columns.join(', ')}, data) VALUES ${items.map(() => placeholders).join(', ')};`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DELETE_STATEMENT = (resource, ids) => {
|
||||||
|
const baseDeleteStatement = `DELETE FROM "${underscore(resource)}"`;
|
||||||
|
return ids?.length > 0
|
||||||
|
? `${baseDeleteStatement} WHERE id IN (${ids.map(() => '?').join(',')})`
|
||||||
|
: baseDeleteStatement;
|
||||||
|
};
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
let clientChannel = null;
|
||||||
|
let commonChannel = null;
|
||||||
|
const requestsInFlight = new Map();
|
||||||
|
|
||||||
|
export default (serviceName, target, onProviderChange) => {
|
||||||
|
commonChannel = new BroadcastChannel(
|
||||||
|
`shared-service-common-channel-${serviceName}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
let { promise: ready, resolve: readyResolve } = Promise.withResolvers();
|
||||||
|
const status = {
|
||||||
|
ready,
|
||||||
|
isServiceProvider: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const proxy = getProxy(target, status);
|
||||||
|
|
||||||
|
// This lock is used to ensure that only one tab can become the provider with an indefinite lock.
|
||||||
|
navigator.locks.request(serviceName, { mode: 'exclusive' }, async () => {
|
||||||
|
// If a client was promoted to provider, reset the ready status and resolver
|
||||||
|
if (readyResolve === null) {
|
||||||
|
const { promise: newReady, resolve: newResolve } =
|
||||||
|
Promise.withResolvers();
|
||||||
|
status.ready = newReady;
|
||||||
|
readyResolve = newResolve;
|
||||||
|
}
|
||||||
|
|
||||||
|
status.isServiceProvider = true;
|
||||||
|
await onBecomeProvider({
|
||||||
|
serviceName,
|
||||||
|
onProviderChange,
|
||||||
|
target,
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
readyResolve();
|
||||||
|
readyResolve = null;
|
||||||
|
await new Promise(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
let providerExists = false;
|
||||||
|
|
||||||
|
// Make sure a lock was acquired which means we have a provider
|
||||||
|
while (!providerExists) {
|
||||||
|
const locks = await navigator.locks.query();
|
||||||
|
providerExists = locks.held?.some((lock) => lock.name === serviceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!status.isServiceProvider) {
|
||||||
|
await onNotProvider({
|
||||||
|
serviceName,
|
||||||
|
onProviderChange,
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
readyResolve();
|
||||||
|
readyResolve = null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return { proxy, status };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getClientBroadcastChannelName = (clientId, serviceName) =>
|
||||||
|
`${clientId}-${serviceName}`;
|
||||||
|
|
||||||
|
// Generate a random ID for the tab to use as a lock name to hold indefinitely until the tab is closed
|
||||||
|
const getClientId = () => {
|
||||||
|
const clientId = uuidv4();
|
||||||
|
|
||||||
|
// Keep the lock until this context is destroyed (which means the tab was closed).
|
||||||
|
navigator.locks.request(clientId, { mode: 'exclusive' }, async () => {
|
||||||
|
await new Promise(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
return clientId;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sends a message to the provider to call the method on behalf of a client.
|
||||||
|
const requestFromProvider = (uuid, resolve, reject, method, args) => {
|
||||||
|
const onRequestListener = getOnRequestListener(uuid, resolve, reject);
|
||||||
|
|
||||||
|
// Listen for a response to know we finished and remove from the list of requests in flight
|
||||||
|
clientChannel?.addEventListener('message', onRequestListener);
|
||||||
|
clientChannel?.postMessage({
|
||||||
|
id: uuid,
|
||||||
|
type: 'request',
|
||||||
|
method,
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return the listener used for responding to requests. Only listens for response messages.
|
||||||
|
const getOnRequestListener = (uuid, resolve, reject) => {
|
||||||
|
const listener = ({ data }) => {
|
||||||
|
const { id, type } = data;
|
||||||
|
if (id !== uuid || type === 'request') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestsInFlight.delete(uuid);
|
||||||
|
clientChannel?.removeEventListener('message', listener);
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
reject(data.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(data.result);
|
||||||
|
};
|
||||||
|
|
||||||
|
return listener;
|
||||||
|
};
|
||||||
|
|
||||||
|
// When we become a client instead of a provider we do two things:
|
||||||
|
// 1. Register ourselves with the provider and wait for a response that says we registered.
|
||||||
|
// 2. Setup an event listener to know when a provider has changed.
|
||||||
|
const onNotProvider = async ({ serviceName, onProviderChange, status }) => {
|
||||||
|
const clientId = getClientId();
|
||||||
|
clientChannel = new BroadcastChannel(
|
||||||
|
getClientBroadcastChannelName(clientId, serviceName),
|
||||||
|
);
|
||||||
|
|
||||||
|
const register = async () =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
const onRegisteredListener = ({ data }) => {
|
||||||
|
if (data.clientId === clientId && data.type === 'registered') {
|
||||||
|
commonChannel.removeEventListener('message', onRegisteredListener);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add an event listener to check for the registration response
|
||||||
|
commonChannel.addEventListener('message', onRegisteredListener);
|
||||||
|
commonChannel.postMessage({ type: 'register', clientId });
|
||||||
|
});
|
||||||
|
|
||||||
|
commonChannel.addEventListener('message', async ({ data }) => {
|
||||||
|
// Check if this was a provider change. If so and we're not the new provider, re-register.
|
||||||
|
if (data.type === 'providerChange' && !status.isServiceProvider) {
|
||||||
|
await onProviderChange?.(status.isServiceProvider);
|
||||||
|
await register();
|
||||||
|
|
||||||
|
// If there were requests in flight when the provider was changed,
|
||||||
|
// we need to resend them to the new provider.
|
||||||
|
if (requestsInFlight.size > 0) {
|
||||||
|
requestsInFlight.forEach(({ method, args, resolve, reject }, uuid) => {
|
||||||
|
requestFromProvider(uuid, resolve, reject, method, args);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await register();
|
||||||
|
};
|
||||||
|
|
||||||
|
// When we become the provider, we do two things:
|
||||||
|
// 1. Set an event listener to check for clients trying to register with us. As part of registration,
|
||||||
|
// we also setup a listener with the individual client to listen for requests.
|
||||||
|
// 2. Send a message to all tabs that we've now become the provider
|
||||||
|
const onBecomeProvider = async ({
|
||||||
|
serviceName,
|
||||||
|
onProviderChange,
|
||||||
|
target,
|
||||||
|
status,
|
||||||
|
}) => {
|
||||||
|
// Set an event listener for other tabs to register with the provider tab
|
||||||
|
commonChannel.addEventListener('message', async (event) => {
|
||||||
|
const { clientId, type } = event.data;
|
||||||
|
if (type !== 'register') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientChannel = new BroadcastChannel(
|
||||||
|
getClientBroadcastChannelName(clientId, serviceName),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Acquire another lock that immediately is waiting for the
|
||||||
|
// initial lock on the client tab to be resolved to handle cleanup.
|
||||||
|
navigator.locks.request(clientId, { mode: 'exclusive' }, async () => {
|
||||||
|
// The client tab was closed, clean up.
|
||||||
|
clientChannel.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup an event listener for communication between the provider and subscriber to respond to requests
|
||||||
|
clientChannel.addEventListener('message', async ({ data }) => {
|
||||||
|
if (data.type === 'response') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { method, args, id } = data;
|
||||||
|
|
||||||
|
let result, error;
|
||||||
|
try {
|
||||||
|
result = await target[method](...args);
|
||||||
|
} catch (e) {
|
||||||
|
// Error objects won't be properly serialized, we need to reconstruct an error like object
|
||||||
|
error =
|
||||||
|
e instanceof Error
|
||||||
|
? Object.fromEntries(
|
||||||
|
Object.getOwnPropertyNames(e).map((k) => [k, e[k]]),
|
||||||
|
)
|
||||||
|
: e;
|
||||||
|
}
|
||||||
|
|
||||||
|
clientChannel.postMessage({
|
||||||
|
id,
|
||||||
|
type: 'response',
|
||||||
|
result,
|
||||||
|
error,
|
||||||
|
method,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send back a message to the client that we finished registering
|
||||||
|
commonChannel.postMessage({ type: 'registered', clientId, serviceName });
|
||||||
|
});
|
||||||
|
|
||||||
|
await onProviderChange?.(status.isServiceProvider);
|
||||||
|
commonChannel.postMessage({ type: 'providerChange', serviceName });
|
||||||
|
|
||||||
|
// If we became the new provider and there were requests in flight, request it directly again.
|
||||||
|
if (requestsInFlight.size > 0) {
|
||||||
|
await Promise.all(
|
||||||
|
Array.from(
|
||||||
|
requestsInFlight,
|
||||||
|
async ([uuid, { method, args, resolve, reject }]) => {
|
||||||
|
try {
|
||||||
|
const result = await target[method](...args);
|
||||||
|
resolve(result);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
} finally {
|
||||||
|
requestsInFlight.delete(uuid);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns a proxy that we will have all caller methods go through.
|
||||||
|
// If we're the provider, we just call the method directly. If we're not the provider, we'll
|
||||||
|
// send the request through our established client channel to have the provider respond with the data.
|
||||||
|
const getProxy = (target, status) =>
|
||||||
|
new Proxy(target, {
|
||||||
|
get: (target, method) => {
|
||||||
|
if (method === 'then' || method === 'catch' || method === 'finally') {
|
||||||
|
// Return undefined for these methods to allow promise chaining to work correctly
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return async (...args) => {
|
||||||
|
if (status.isServiceProvider) {
|
||||||
|
return await target[method](...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const uuid = uuidv4();
|
||||||
|
requestFromProvider(uuid, resolve, reject, method, args);
|
||||||
|
requestsInFlight.set(uuid, { method, args, resolve, reject });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -3,4 +3,4 @@
|
|||||||
* SPDX-License-Identifier: BUSL-1.1
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@forward 'button';
|
export { default } from 'api/services/db';
|
||||||
+1
-1
@@ -3,4 +3,4 @@
|
|||||||
* SPDX-License-Identifier: BUSL-1.1
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@import 'link';
|
export { default } from 'api/services/sqlite';
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Ember-data no longer automatically installs BooleanTransform for apps.
|
||||||
|
export { BooleanTransform as default } from '@ember-data/serializer/transform';
|
||||||
@@ -3,4 +3,4 @@
|
|||||||
* SPDX-License-Identifier: BUSL-1.1
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { default, initialize } from 'core/initializers/deprecations';
|
export { DateTransform as default } from '@ember-data/serializer/transform';
|
||||||
@@ -3,4 +3,4 @@
|
|||||||
* SPDX-License-Identifier: BUSL-1.1
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { default } from 'rose/components/rose/button';
|
export { NumberTransform as default } from '@ember-data/serializer/transform';
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Ember-data no longer automatically installs StringTransform for apps.
|
||||||
|
export { StringTransform as default } from '@ember-data/serializer/transform';
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
const Plugin = require('broccoli-plugin');
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const esbuild = require('esbuild');
|
||||||
|
|
||||||
|
module.exports = class BuildWorkers extends Plugin {
|
||||||
|
build() {
|
||||||
|
this.inputPaths.forEach((inputPath) => {
|
||||||
|
this.#buildWorkers(inputPath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#buildWorkers(inputPath) {
|
||||||
|
const workers = this.#getWorkers(inputPath);
|
||||||
|
|
||||||
|
const workerBuilder = this.#configureWorkerBuilder({
|
||||||
|
isProduction: this.isProduction,
|
||||||
|
buildDir: this.outputPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.entries(workers).map(workerBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
#getWorkers(inputPath) {
|
||||||
|
let workers = {};
|
||||||
|
let dir = fs.readdirSync(inputPath);
|
||||||
|
|
||||||
|
dir
|
||||||
|
.filter((name) => name.endsWith('-worker.js'))
|
||||||
|
.forEach((name) => {
|
||||||
|
workers[name] = path.join(inputPath, name);
|
||||||
|
});
|
||||||
|
|
||||||
|
return workers;
|
||||||
|
}
|
||||||
|
|
||||||
|
#configureWorkerBuilder({ isProduction, buildDir }) {
|
||||||
|
return ([name, entryPath]) => {
|
||||||
|
esbuild.buildSync({
|
||||||
|
loader: { '.js': 'js' },
|
||||||
|
entryPoints: [entryPath],
|
||||||
|
bundle: true,
|
||||||
|
outfile: path.join(buildDir, name),
|
||||||
|
format: 'esm',
|
||||||
|
minify: isProduction,
|
||||||
|
sourcemap: !isProduction,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const EmberAddon = require('ember-cli/lib/broccoli/ember-addon');
|
const EmberAddon = require('ember-cli/lib/broccoli/ember-addon');
|
||||||
const { maybeEmbroider } = require('@embroider/test-setup');
|
|
||||||
|
|
||||||
module.exports = function (defaults) {
|
module.exports = function (defaults) {
|
||||||
const app = new EmberAddon(defaults, {
|
const app = new EmberAddon(defaults, {
|
||||||
@@ -19,6 +18,8 @@ module.exports = function (defaults) {
|
|||||||
This build file does *not* influence how the addon or the app using it
|
This build file does *not* influence how the addon or the app using it
|
||||||
behave. You most likely want to be modifying `./index.js` or app's build file
|
behave. You most likely want to be modifying `./index.js` or app's build file
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const { maybeEmbroider } = require('@embroider/test-setup');
|
||||||
return maybeEmbroider(app, {
|
return maybeEmbroider(app, {
|
||||||
skipBabel: [
|
skipBabel: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,69 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const { WatchedDir } = require('broccoli-source');
|
||||||
|
const funnel = require('broccoli-funnel');
|
||||||
|
const merge = require('broccoli-merge-trees');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const buildWorkers = require('./broccoli-plugins/build-workers');
|
||||||
|
|
||||||
|
const workerRoot = path.join(__dirname, 'addon', 'workers');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: require('./package').name,
|
name: require('./package').name,
|
||||||
|
|
||||||
|
options: {
|
||||||
|
// Don't automatically import sqlite, we need to manually control how it gets used
|
||||||
|
autoImport: {
|
||||||
|
exclude: ['@sqlite.org/sqlite-wasm'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
treeForAddon() {
|
||||||
|
// Exclude anything in the workers folder from being bundled in the final
|
||||||
|
// build as we're manually bundling the files ourselves below.
|
||||||
|
const tree = this._super.treeForAddon.apply(this, arguments);
|
||||||
|
return funnel(tree, {
|
||||||
|
exclude: ['api/workers/**/*'],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
treeForPublic(tree) {
|
||||||
|
const env = this.parent?.app?.env ?? 'production';
|
||||||
|
const { enableSqlite } = this.parent?.app?.options[this.name] ?? {};
|
||||||
|
|
||||||
|
// If sqlite is not enabled and we're not running a test directly in the addon,
|
||||||
|
// we don't need to build the workers
|
||||||
|
if (!enableSqlite && this.app?.env !== 'test') {
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to process and build our workers separately from ember as these
|
||||||
|
// workers are not part of the app and are not getting properly bundled otherwise
|
||||||
|
let workerTree = new WatchedDir(workerRoot);
|
||||||
|
workerTree = new buildWorkers([workerTree]);
|
||||||
|
workerTree.isProduction = env === 'production';
|
||||||
|
|
||||||
|
const workers = funnel(workerTree, {
|
||||||
|
destDir: 'workers',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also include the sqlite wasm file directly to be placed next to the workers
|
||||||
|
const sqliteWasmDirectoryPath = path.resolve(
|
||||||
|
require.resolve('@sqlite.org/sqlite-wasm/sqlite3.wasm'),
|
||||||
|
'..',
|
||||||
|
);
|
||||||
|
const wasmFile = funnel(sqliteWasmDirectoryPath, {
|
||||||
|
files: ['sqlite3.wasm'],
|
||||||
|
destDir: 'workers',
|
||||||
|
});
|
||||||
|
|
||||||
|
const trees = [workers, wasmFile];
|
||||||
|
if (tree) {
|
||||||
|
trees.push(tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
return merge(trees);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
Loaded 100 of 1727 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user