mirror of
https://github.com/OliveTin/OliveTin
synced 2025-12-11 08:35:37 +00:00
Compare commits
327 Commits
2023.03.24
...
2024.09.02
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8c330aae3 | ||
|
|
d4bd7dd586 | ||
|
|
fa20f6a63a | ||
|
|
b9ce695616 | ||
|
|
3f1dbf1130 | ||
|
|
6413e51cf5 | ||
|
|
defcf6d26e | ||
|
|
2671983c43 | ||
|
|
dc9653307b | ||
|
|
eb91eb33d5 | ||
|
|
ddb803d9b5 | ||
|
|
c9095b4d67 | ||
|
|
40158eda71 | ||
|
|
bbbbfceeb3 | ||
|
|
9ca1940834 | ||
|
|
37160a91f3 | ||
|
|
274d036f74 | ||
|
|
1fe0e49adb | ||
|
|
7a7a07d9ad | ||
|
|
5b5fca0837 | ||
|
|
fab0264d9b | ||
|
|
8d839ee6ce | ||
|
|
90efbf3159 | ||
|
|
17dd1b4158 | ||
|
|
e5f6d8ff50 | ||
|
|
e183910b88 | ||
|
|
8cd5b9fb46 | ||
|
|
6958445f83 | ||
|
|
ef91294d5e | ||
|
|
a36f286f8a | ||
|
|
d9e921950f | ||
|
|
ffcd19e748 | ||
|
|
d0eb132b95 | ||
|
|
1cf971c092 | ||
|
|
49f745be68 | ||
|
|
b50824a705 | ||
|
|
69d1cc75a7 | ||
|
|
a54ea505c9 | ||
|
|
a1563b72ae | ||
|
|
31d7168aac | ||
|
|
09016e1d5f | ||
|
|
652882350c | ||
|
|
20c4423799 | ||
|
|
bb90a5da92 | ||
|
|
3ca3a2dd3c | ||
|
|
510c48e1af | ||
|
|
3ac809c234 | ||
|
|
9dd33bc3f9 | ||
|
|
897cc0e034 | ||
|
|
482ef0e5e8 | ||
|
|
e0678fc0a9 | ||
|
|
9cb5574b99 | ||
|
|
c18b91f684 | ||
|
|
6622a6ded4 | ||
|
|
fb6aaa52c7 | ||
|
|
a1adc2a85d | ||
|
|
3d9cb621dd | ||
|
|
943b4c75aa | ||
|
|
fb972fae55 | ||
|
|
eb4f28dfda | ||
|
|
e36fedf4b2 | ||
|
|
362a97c59e | ||
|
|
c82beb61a9 | ||
|
|
00a8a0bf69 | ||
|
|
ffc17dd73b | ||
|
|
238abc95ad | ||
|
|
ac0f3ab6f8 | ||
|
|
4bac315568 | ||
|
|
daa48b5a73 | ||
|
|
c70cc864ee | ||
|
|
dc7ff40da6 | ||
|
|
046ffaecf4 | ||
|
|
3904f8563d | ||
|
|
18423a9888 | ||
|
|
8fbbd9b32c | ||
|
|
7e34efd453 | ||
|
|
9d33e62d34 | ||
|
|
6b2bc0adc0 | ||
|
|
80083fedab | ||
|
|
1ab35fdb36 | ||
|
|
f43ece4263 | ||
|
|
ea1ce82ded | ||
|
|
c24adaafcb | ||
|
|
c42875b107 | ||
|
|
447ad36d4d | ||
|
|
71dc467b31 | ||
|
|
8625e1fc0a | ||
|
|
638e5b7fe1 | ||
|
|
f467c69e8f | ||
|
|
8fd98874e2 | ||
|
|
dc6f6c2896 | ||
|
|
a783fc8cd4 | ||
|
|
6a16a7c6c2 | ||
|
|
ceb215a6dc | ||
|
|
d6cb634824 | ||
|
|
500419307b | ||
|
|
12cf0013e2 | ||
|
|
318d4fe0d0 | ||
|
|
a3aee3603f | ||
|
|
7c6f36c600 | ||
|
|
dde8a9cbb6 | ||
|
|
dfca712cb1 | ||
|
|
5057ba2e1c | ||
|
|
8f1dfffa49 | ||
|
|
6991724258 | ||
|
|
09e1de06ce | ||
|
|
5a644b0856 | ||
|
|
86b2187236 | ||
|
|
fafacfeafb | ||
|
|
362019738d | ||
|
|
1a0ba6c6b1 | ||
|
|
4c2cc5da1c | ||
|
|
30e2aa141b | ||
|
|
3768a57eaa | ||
|
|
db5de9be97 | ||
|
|
f60ab6ce85 | ||
|
|
43c48aef17 | ||
|
|
8341b1d6d0 | ||
|
|
dd11961a11 | ||
|
|
3de819a0e9 | ||
|
|
0b546eaeb5 | ||
|
|
558e1819bf | ||
|
|
9d6afa2fe7 | ||
|
|
5f3a967515 | ||
|
|
179f1be19a | ||
|
|
f13a5c070a | ||
|
|
9476d052b6 | ||
|
|
1d446ace04 | ||
|
|
3a8d8706a6 | ||
|
|
910418925a | ||
|
|
1b539df2aa | ||
|
|
f5794e57ee | ||
|
|
7dd1d0a7fc | ||
|
|
ce670cf58c | ||
|
|
c4d1a2a105 | ||
|
|
709223bd46 | ||
|
|
19b4340e18 | ||
|
|
a5a1c64dcb | ||
|
|
2b7bdffe41 | ||
|
|
a2df96354e | ||
|
|
8b49eeff98 | ||
|
|
a8c4db197d | ||
|
|
1319f314ff | ||
|
|
781abaaf40 | ||
|
|
744debc00a | ||
|
|
77321c1bcd | ||
|
|
c97fd60c25 | ||
|
|
2fb40ff443 | ||
|
|
a992ef84f5 | ||
|
|
8fce4c79b6 | ||
|
|
7e4aa9ebbe | ||
|
|
522e5bb129 | ||
|
|
1a97836dc3 | ||
|
|
e1bc9276bc | ||
|
|
555b6929e4 | ||
|
|
ee4d61e476 | ||
|
|
f15235d120 | ||
|
|
471d5726f6 | ||
|
|
e344530fc0 | ||
|
|
29fe38eff4 | ||
|
|
25e643371e | ||
|
|
32cb8dd873 | ||
|
|
12cc61fba5 | ||
|
|
fe40731df3 | ||
|
|
7464ca5543 | ||
|
|
ce83521429 | ||
|
|
27ab530ba6 | ||
|
|
843121f5fd | ||
|
|
d26c469107 | ||
|
|
97453260eb | ||
|
|
06b85c5769 | ||
|
|
5bb21031ac | ||
|
|
256d6139b7 | ||
|
|
aa342047ed | ||
|
|
ea663f8286 | ||
|
|
e953dfb017 | ||
|
|
54170f3da6 | ||
|
|
77ec2fea63 | ||
|
|
83beab4c92 | ||
|
|
29b6d12454 | ||
|
|
866a38f286 | ||
|
|
0ec2e7069b | ||
|
|
f3934b1906 | ||
|
|
fb2bb63d15 | ||
|
|
58cc04298f | ||
|
|
42535feadf | ||
|
|
baa690ffc2 | ||
|
|
d13f0a7acf | ||
|
|
b747199528 | ||
|
|
6a1af44aa0 | ||
|
|
6da050e3b9 | ||
|
|
b8f23ce80c | ||
|
|
865bef532a | ||
|
|
6fb158190d | ||
|
|
0e3f9c8ceb | ||
|
|
4dba6fd0f9 | ||
|
|
fbbf168e88 | ||
|
|
2cd739c3b4 | ||
|
|
a8e770726a | ||
|
|
0c5a99cc03 | ||
|
|
2dee246593 | ||
|
|
381bf59fbd | ||
|
|
fddf83f27d | ||
|
|
3d3e19e26a | ||
|
|
c082a5438a | ||
|
|
5adab1091f | ||
|
|
290a2ec91b | ||
|
|
9ebeabac51 | ||
|
|
b5e2c8d6b8 | ||
|
|
a482b6a3c2 | ||
|
|
f348de6a03 | ||
|
|
8df8978516 | ||
|
|
086d8fd21c | ||
|
|
7dce77adcf | ||
|
|
917a0469d8 | ||
|
|
c12431d8a3 | ||
|
|
dc0cf33d37 | ||
|
|
15d332012f | ||
|
|
99460beafd | ||
|
|
5b0cfb5c33 | ||
|
|
759e747f54 | ||
|
|
6892a679ee | ||
|
|
1b13a2bc4b | ||
|
|
63e8e10a6d | ||
|
|
0615a7e353 | ||
|
|
16acf9db91 | ||
|
|
0c19ba59d2 | ||
|
|
143528d919 | ||
|
|
07cdf378f6 | ||
|
|
f559a2f9c9 | ||
|
|
6b3e9e4676 | ||
|
|
3f72b7cc0d | ||
|
|
4ce5b0e645 | ||
|
|
e92ab8d741 | ||
|
|
6b0e414932 | ||
|
|
00927f3ba3 | ||
|
|
b0faecfa75 | ||
|
|
c15449f99a | ||
|
|
68f04c0912 | ||
|
|
c3c010443f | ||
|
|
15c8abf3d6 | ||
|
|
d0f74c1ab7 | ||
|
|
ca921c1890 | ||
|
|
3b60bbce0a | ||
|
|
8f6b384fe6 | ||
|
|
4d04264caa | ||
|
|
912fd8089e | ||
|
|
5739091773 | ||
|
|
a09c278585 | ||
|
|
a7fb49a11b | ||
|
|
3db8ae53b5 | ||
|
|
311f9a1d00 | ||
|
|
50204f8180 | ||
|
|
d639a802dc | ||
|
|
f41eafe3bd | ||
|
|
8b080eb3cc | ||
|
|
268d8a3a90 | ||
|
|
44d6c40c27 | ||
|
|
9522e25b1d | ||
|
|
db475895ca | ||
|
|
dc33509127 | ||
|
|
2ada67be04 | ||
|
|
77b17604f3 | ||
|
|
d169b3f2b1 | ||
|
|
c8210568eb | ||
|
|
2ea6430a3b | ||
|
|
ac5f997f85 | ||
|
|
e1930c0899 | ||
|
|
020281c1e6 | ||
|
|
9dc81a6280 | ||
|
|
a9906addff | ||
|
|
37269cef02 | ||
|
|
186ec00de7 | ||
|
|
abd13b756c | ||
|
|
6851683d94 | ||
|
|
2b4a3ab137 | ||
|
|
6d21bbe03a | ||
|
|
6f4a0e68a7 | ||
|
|
3de0f38049 | ||
|
|
216ccbfcef | ||
|
|
66b012cd55 | ||
|
|
8339bd3cb1 | ||
|
|
e93c62b06d | ||
|
|
43ceb33b53 | ||
|
|
ae0b45c308 | ||
|
|
bbaeff00f3 | ||
|
|
75a9697586 | ||
|
|
77bd37ca90 | ||
|
|
3a44a6a3b4 | ||
|
|
604d956d0c | ||
|
|
822327edfc | ||
|
|
3ea14f1353 | ||
|
|
7dbc077f76 | ||
|
|
e8cb661938 | ||
|
|
e5a870ed94 | ||
|
|
6116e954ba | ||
|
|
56ef7ce95c | ||
|
|
6e2e585175 | ||
|
|
8c1c0c6029 | ||
|
|
11dad79794 | ||
|
|
4b3485145f | ||
|
|
4b5a579b0b | ||
|
|
07bd09473c | ||
|
|
adba3b0d4b | ||
|
|
f6162c58f2 | ||
|
|
ed949d1dd8 | ||
|
|
5d94de418b | ||
|
|
f7fd8af124 | ||
|
|
d74734972b | ||
|
|
8736f5e387 | ||
|
|
89f08ae6c7 | ||
|
|
0b75b3847d | ||
|
|
6d27c3db11 | ||
|
|
b78065e23c | ||
|
|
c611c5c749 | ||
|
|
5b637154ea | ||
|
|
0b6edb3c38 | ||
|
|
f1ba1c55a6 | ||
|
|
2940a63d09 | ||
|
|
cc311f88a5 | ||
|
|
0911df0442 | ||
|
|
86e5dfe2ee | ||
|
|
34b5570563 | ||
|
|
2d806d8557 | ||
|
|
a792a0aa55 | ||
|
|
e7ab8441d7 | ||
|
|
d0b7efa24c |
44
.air.toml
Normal file
44
.air.toml
Normal file
@@ -0,0 +1,44 @@
|
||||
root = "."
|
||||
testdata_dir = "testdata"
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
args_bin = []
|
||||
bin = "./OliveTin"
|
||||
cmd = "go build -o OliveTin github.com/OliveTin/OliveTin/cmd/OliveTin"
|
||||
delay = 1
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata", "webui.dev", "webui"]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
full_bin = ""
|
||||
include_dir = []
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
include_file = []
|
||||
kill_delay = "0s"
|
||||
log = "build-errors.log"
|
||||
poll = false
|
||||
poll_interval = 0
|
||||
rerun = false
|
||||
rerun_delay = 500
|
||||
send_interrupt = false
|
||||
stop_on_error = true
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
|
||||
[log]
|
||||
main_only = false
|
||||
time = false
|
||||
|
||||
[misc]
|
||||
clean_on_exit = false
|
||||
|
||||
[screen]
|
||||
clear_on_rebuild = false
|
||||
keep_scroll = true
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
#!/usr/bin/env python
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/support_request.md
vendored
2
.github/ISSUE_TEMPLATE/support_request.md
vendored
@@ -39,7 +39,7 @@ If possible, please copy and paste your OliveTin logs from when the error happen
|
||||
**Screenshot of WebDeveloper console logs**
|
||||
|
||||
If you know how, and if you think it's relevant, a screenshot of the
|
||||
WebDeveloper console from when you clicked a button is often really helpful.
|
||||
WebDeveloper console from when you clicked a button is often really helpful.
|
||||
|
||||
**Anything else?**
|
||||
|
||||
|
||||
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -4,7 +4,7 @@ First of all, thank you for considering to raise a pull request!
|
||||
|
||||
Don’t be afraid to ask for advice before working on a contribution. If you’re thinking about a bigger change, especially that might affect the core working or architecture, it’s almost essential to talk and ask about what you’re planning might affect things. Some of the larger future plans may not be documented well so it’s difficult to understand how your change might affect the general direction and roadmap of this project without asking.
|
||||
|
||||
The preferred way to communicate is probably via Discord or GitHub issues.
|
||||
The preferred way to communicate is probably via Discord or GitHub issues.
|
||||
|
||||
Helpful information to understand the project can be found here: [CONTRIBUTING](https://github.com/OliveTin/OliveTin/blob/main/CONTRIBUTING.adoc)
|
||||
|
||||
@@ -13,10 +13,10 @@ Helpful information to understand the project can be found here: [CONTRIBUTING](
|
||||
# Checklist
|
||||
Please put a X in the boxes as evidence of reading through the checklist.
|
||||
|
||||
- [ ] I have forked the project, and raised this PR on a feature branch.
|
||||
- [ ] I have forked the project, and raised this PR on a feature branch.
|
||||
- [ ] `make githooks` has been run, and my git commit message was accepted by the git hook.
|
||||
- [ ] `make daemon-compile` runs without any issues.
|
||||
- [ ] `make daemon-codestyle` runs without any issues.
|
||||
- [ ] `make daemon-unittests` runs without any issues.
|
||||
- [ ] `make webui-codestyle` runs without any issues.
|
||||
- [ ] I understand and accept the [AGPL-3.0 license](LICENSE) and [code of conduct](CODE_OF_CONDUCT.md), and my contributions fall under these.
|
||||
- [ ] `make webui-codestyle` runs without any issues.
|
||||
- [ ] I understand and accept the [AGPL-3.0 license](LICENSE) and [code of conduct](CODE_OF_CONDUCT.md), and my contributions fall under these.
|
||||
|
||||
58
.github/workflows/build-snapshot.yml
vendored
58
.github/workflows/build-snapshot.yml
vendored
@@ -2,53 +2,79 @@
|
||||
name: "Build Snapshot"
|
||||
|
||||
on:
|
||||
- push
|
||||
- workflow_dispatch
|
||||
push:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build-snapshot:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref_type != 'tag'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
image: tonistiigi/binfmt:latest
|
||||
platforms: arm64,arm
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: webui/package-lock.json
|
||||
cache-dependency-path: webui.dev/package-lock.json
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '>=1.18.0'
|
||||
go-version-file: 'go.mod'
|
||||
cache: true
|
||||
|
||||
- name: Print go version
|
||||
run: go version
|
||||
|
||||
- name: grpc
|
||||
run: make grpc
|
||||
run: make -w grpc
|
||||
|
||||
- name: make daemon
|
||||
run: make -w daemon-compile-x64-lin
|
||||
|
||||
- name: make webui
|
||||
run: make -w webui-dist
|
||||
|
||||
- name: unit tests
|
||||
run: make -w daemon-unittests
|
||||
|
||||
- name: integration tests
|
||||
run: cd integration-tests && make -w
|
||||
|
||||
- name: goreleaser
|
||||
uses: goreleaser/goreleaser-action@v4.2.0
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --snapshot --clean --parallelism 1 --skip-docker
|
||||
args: release --snapshot --clean --parallelism 1 --skip=docker
|
||||
|
||||
- name: get date
|
||||
run: |
|
||||
echo "DATE=$(date +'%Y-%m-%d')" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Archive binaries
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
with:
|
||||
name: "OliveTin-snapshot-${{ github.sha }}-dist"
|
||||
name: "OliveTin-snapshot-${{ env.DATE }}-${{ github.sha }}"
|
||||
path: dist/OliveTin*.*
|
||||
|
||||
- name: Archive integration tests
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
with:
|
||||
name: integration-tests
|
||||
path: integration-tests
|
||||
name: "OliveTin-integration-tests-${{ env.DATE }}-${{ github.sha }}"
|
||||
path: |
|
||||
integration-tests
|
||||
!integration-tests/node_modules
|
||||
|
||||
47
.github/workflows/build-tag.yml
vendored
47
.github/workflows/build-tag.yml
vendored
@@ -11,53 +11,70 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
image: tonistiigi/binfmt:latest
|
||||
platforms: arm64,arm
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: webui/package-lock.json
|
||||
cache-dependency-path: webui.dev/package-lock.json
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '^1.18.0'
|
||||
go-version-file: 'go.mod'
|
||||
cache: true
|
||||
|
||||
- name: Print go version
|
||||
run: go version
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_KEY }}
|
||||
|
||||
- name: Login to ghcr
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.CONTAINER_TOKEN }}
|
||||
|
||||
- name: grpc
|
||||
run: make grpc
|
||||
run: make -w grpc
|
||||
|
||||
- name: make webui
|
||||
run: make -w webui-dist
|
||||
|
||||
- name: goreleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --rm-dist --parallelism 1
|
||||
args: release --clean --parallelism 1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.CONTAINER_TOKEN }}
|
||||
|
||||
- name: Archive binaries
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
with:
|
||||
name: dist
|
||||
name: "OliveTin-${{ github.ref_name }}"
|
||||
path: dist/OliveTin*.*
|
||||
|
||||
- name: Archive integration tests
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
with:
|
||||
name: integration-tests
|
||||
path: integration-tests
|
||||
path: |
|
||||
integration-tests
|
||||
!integration-tests/node_modules
|
||||
|
||||
19
.github/workflows/codeql-analysis.yml
vendored
19
.github/workflows/codeql-analysis.yml
vendored
@@ -17,7 +17,7 @@ on:
|
||||
paths:
|
||||
- 'cmd/**'
|
||||
- 'internal/**'
|
||||
- 'webui/**'
|
||||
- 'webui.dev/**'
|
||||
- 'integration-tests/**'
|
||||
- 'OliveTin.proto'
|
||||
branches: [main]
|
||||
@@ -42,13 +42,24 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
cache: true
|
||||
|
||||
- name: grpc
|
||||
run: make -w grpc
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
18
.github/workflows/codestyle.yml
vendored
18
.github/workflows/codestyle.yml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
paths:
|
||||
- 'cmd/**'
|
||||
- 'internal/**'
|
||||
- 'webui/**'
|
||||
- 'webui.dev/**'
|
||||
- 'integration-tests/**'
|
||||
- 'OliveTin.proto'
|
||||
|
||||
@@ -16,18 +16,22 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '^1.16.0'
|
||||
go-version-file: 'go.mod'
|
||||
cache: true
|
||||
|
||||
- name: Print go version
|
||||
run: go version
|
||||
|
||||
- name: deps
|
||||
run: make grpc
|
||||
run: make -w grpc
|
||||
|
||||
- name: daemon
|
||||
run: make daemon-codestyle
|
||||
run: make -w daemon-codestyle
|
||||
|
||||
- name: webui
|
||||
run: make webui-codestyle
|
||||
run: make -w webui-codestyle
|
||||
|
||||
34
.github/workflows/devskim.yml
vendored
Normal file
34
.github/workflows/devskim.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
name: DevSkim
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
schedule:
|
||||
- cron: '34 21 * * 2'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: DevSkim
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Run DevSkim scanner
|
||||
uses: microsoft/DevSkim-Action@v1
|
||||
|
||||
- name: Upload DevSkim scan results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
with:
|
||||
sarif_file: devskim-results.sarif
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,4 +1,3 @@
|
||||
webui/node_modules
|
||||
**/*.swp
|
||||
**/*.swo
|
||||
gen/
|
||||
@@ -9,3 +8,8 @@ reports
|
||||
releases/
|
||||
dist/
|
||||
installation-id.txt
|
||||
tmp/
|
||||
webui/
|
||||
webui.dev/node_modules
|
||||
webui.dev/.parcel-cache
|
||||
custom-webui
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
project_name: OliveTin
|
||||
version: 2
|
||||
before:
|
||||
hooks:
|
||||
- go mod download
|
||||
- rm -rf webui/node_modules
|
||||
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
@@ -18,7 +19,7 @@ builds:
|
||||
|
||||
goarm:
|
||||
- 5 # For old RPIs
|
||||
- 6
|
||||
- 6
|
||||
- 7
|
||||
|
||||
main: cmd/OliveTin/main.go
|
||||
@@ -62,10 +63,10 @@ changelog:
|
||||
- '^refactor:'
|
||||
|
||||
archives:
|
||||
-
|
||||
-
|
||||
format: tar.gz
|
||||
|
||||
files:
|
||||
files:
|
||||
- config.yaml
|
||||
- LICENSE
|
||||
- README.md
|
||||
@@ -74,10 +75,6 @@ archives:
|
||||
- OliveTin.service
|
||||
- ./var/
|
||||
|
||||
replacements:
|
||||
darwin: macOS
|
||||
arm: arm32v
|
||||
|
||||
name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}{{ .Arm }}"
|
||||
|
||||
wrap_in_directory: true
|
||||
@@ -96,27 +93,30 @@ dockers:
|
||||
skip_push: false
|
||||
build_flag_templates:
|
||||
- "--platform=linux/amd64"
|
||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||
- "--label=org.opencontainers.image.version={{.Tag}}"
|
||||
- "--platform=linux/amd64"
|
||||
extra_files:
|
||||
- webui
|
||||
- var/entities/
|
||||
- config.yaml
|
||||
- var/helper-actions/
|
||||
|
||||
- image_templates:
|
||||
- "docker.io/jamesread/olivetin:{{ .Tag }}-arm64"
|
||||
- "ghcr.io/olivetin/olivetin:{{ .Tag }}-amd64"
|
||||
- "ghcr.io/olivetin/olivetin:{{ .Tag }}-arm64"
|
||||
dockerfile: Dockerfile.arm64
|
||||
goos: linux
|
||||
goarch: arm64
|
||||
skip_push: false
|
||||
build_flag_templates:
|
||||
- "--platform=linux/arm64"
|
||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||
- "--label=org.opencontainers.image.version={{.Tag}}"
|
||||
extra_files:
|
||||
- webui
|
||||
- var/entities/
|
||||
- config.yaml
|
||||
- var/helper-actions/
|
||||
|
||||
docker_manifests:
|
||||
- name_template: docker.io/jamesread/olivetin:{{ .Version }}
|
||||
@@ -157,28 +157,17 @@ nfpms:
|
||||
- src: OliveTin.service
|
||||
dst: /etc/systemd/system/OliveTin.service
|
||||
|
||||
- src: webui/main.js
|
||||
- src: webui/*
|
||||
dst: /var/www/olivetin/
|
||||
|
||||
- src: webui/index.html
|
||||
dst: /var/www/olivetin/
|
||||
|
||||
- src: webui/*.png
|
||||
dst: /var/www/olivetin/
|
||||
|
||||
- src: webui/*.svg
|
||||
dst: /var/www/olivetin/
|
||||
|
||||
- src: webui/style.css
|
||||
dst: /var/www/olivetin/
|
||||
|
||||
- src: webui/js/
|
||||
dst: /var/www/olivetin/js/
|
||||
|
||||
- src: config.yaml
|
||||
dst: /etc/OliveTin/config.yaml
|
||||
type: "config|noreplace"
|
||||
|
||||
- src: var/entities/*
|
||||
dst: /etc/OliveTin/entities/
|
||||
type: "config|noreplace"
|
||||
|
||||
- src: var/manpage/OliveTin.1.gz
|
||||
dst: /usr/share/man/man1/OliveTin.1.gz
|
||||
|
||||
@@ -198,28 +187,17 @@ nfpms:
|
||||
- src: var/openrc/OliveTin
|
||||
dst: /etc/init.d/OliveTin
|
||||
|
||||
- src: webui/main.js
|
||||
- src: webui/*
|
||||
dst: /var/www/olivetin/
|
||||
|
||||
- src: webui/index.html
|
||||
dst: /var/www/olivetin/
|
||||
|
||||
- src: webui/*.png
|
||||
dst: /var/www/olivetin/
|
||||
|
||||
- src: webui/*.svg
|
||||
dst: /var/www/olivetin/
|
||||
|
||||
- src: webui/style.css
|
||||
dst: /var/www/olivetin/
|
||||
|
||||
- src: webui/js/
|
||||
dst: /var/www/olivetin/js/
|
||||
|
||||
- src: config.yaml
|
||||
dst: /etc/OliveTin/config.yaml
|
||||
type: "config|noreplace"
|
||||
|
||||
- src: var/entities/*
|
||||
dst: /etc/OliveTin/entities/
|
||||
type: "config|noreplace"
|
||||
|
||||
- src: var/manpage/OliveTin.1.gz
|
||||
dst: /usr/share/man/man1/OliveTin.1.gz
|
||||
|
||||
@@ -233,10 +211,13 @@ release:
|
||||
|
||||
- `docker pull docker.io/jamesread/olivetin:{{ .Version }}`
|
||||
|
||||
## Upgrade warnings, or breaking changes
|
||||
|
||||
- No such issues between the last release and this version.
|
||||
|
||||
## Useful links
|
||||
|
||||
- [Which download do I need?](https://docs.olivetin.app/choose-package.html)
|
||||
- [Ask for help and chat with others users in the Discord community](https://discord.gg/jhYWWpNJ3v)
|
||||
|
||||
|
||||
Thanks for your interest in OliveTin!
|
||||
|
||||
|
||||
10
.pre-commit-config.yaml
Normal file
10
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v3.2.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
@@ -7,9 +7,9 @@ of multiple projects, your time and interest in contributing is most welcome.
|
||||
If you're not sure where to get started, raise an issue in the project.
|
||||
|
||||
Ideas may be discussed, purely on their merits and issues. Our Code of Conduct
|
||||
(CoC) is straightforward - it's important that contributors feel comfortable in
|
||||
discussion throughout the whole process. This project respects the
|
||||
link:https://www.kernel.org/doc/html/latest/process/code-of-conduct.html[Linux Kernel code of conduct].
|
||||
(CoC) is straightforward - it's important that contributors feel comfortable in
|
||||
discussion throughout the whole process. This project respects the
|
||||
link:https://www.kernel.org/doc/html/latest/process/code-of-conduct.html[Linux Kernel code of conduct].
|
||||
|
||||
== If you're not sure, ask!
|
||||
|
||||
@@ -18,19 +18,27 @@ contribution. If you're thinking about a bigger change, especially that might
|
||||
affect the core working or architecture, it's almost essential to talk and ask
|
||||
about what you're planning might affect things. Some of the larger future plans may not be
|
||||
documented well so it's difficult to understand how your change might affect
|
||||
the general direction and roadmap of this project without asking.
|
||||
the general direction and roadmap of this project without asking.
|
||||
|
||||
The preferred way to communicate is probably via Discord or GitHub issues.
|
||||
The preferred way to communicate is probably via Discord or GitHub issues.
|
||||
|
||||
=== Dev environment setup and clean build - Fedora
|
||||
=== Dev environment setup and clean build
|
||||
|
||||
```
|
||||
dnf install git go protobuf-compiler make -y
|
||||
# Step1: setup compile env
|
||||
# - Fedora
|
||||
dnf install git go protobuf-compiler make -y
|
||||
# - Windows with chocolatey
|
||||
choco install git go protoc make python nodejs-lts -y
|
||||
|
||||
# Step2: clone and setup repo
|
||||
git clone https://github.com/OliveTin/OliveTin.git
|
||||
cd OliveTin
|
||||
make githooks
|
||||
|
||||
# `make grpc` will also run `make go-tools`, which installs "buf". This binary
|
||||
# will be put in your GOPATH/bin/, which should be on your path. buf is used to
|
||||
# Step3: compile binary for current dev env (OS, ARCH)
|
||||
# `make grpc` will also run `make go-tools`, which installs "buf". This binary
|
||||
# will be put in your GOPATH/bin/, which should be on your path. buf is used to
|
||||
# generate the protobuf / grpc stubs.
|
||||
make grpc
|
||||
make
|
||||
@@ -39,10 +47,10 @@ make
|
||||
|
||||
=== Getting started to contribute;
|
||||
|
||||
The project layout is reasonably straightforward;
|
||||
The project layout is reasonably straightforward;
|
||||
|
||||
* See the `Makefile` for common targets. This project was originally created on top of Fedora, but it should be usable on Debian/your faveourite distro with minor changes (if any).
|
||||
* The API is defined in protobuf+grpc - you will need to `make grpc`.
|
||||
* The API is defined in protobuf+grpc - you will need to `make grpc`.
|
||||
* The Go daemon is built from the `cmd` and `internal` directories mostly.
|
||||
* The webui is just a single page application with a bit of Javascript in the `webui` directory. This can happily be hosted on another webserver.
|
||||
|
||||
|
||||
18
Dockerfile
18
Dockerfile
@@ -1,23 +1,31 @@
|
||||
FROM --platform=linux/amd64 registry.fedoraproject.org/fedora-minimal:36-x86_64
|
||||
FROM --platform=linux/amd64 registry.fedoraproject.org/fedora-minimal:40-x86_64
|
||||
|
||||
LABEL org.opencontainers.image.source https://github.com/OliveTin/OliveTin
|
||||
LABEL org.opencontainers.image.title=OliveTin
|
||||
|
||||
RUN mkdir -p /config /var/www/olivetin \
|
||||
&& microdnf install -y --nodocs --noplugins --setopt=keepcache=0 --setopt=install_weak_deps=0 \
|
||||
RUN mkdir -p /config /config/entities/ /var/www/olivetin \
|
||||
&& \
|
||||
microdnf install -y --nodocs --noplugins --setopt=keepcache=0 --setopt=install_weak_deps=0 \
|
||||
iputils \
|
||||
openssh-clients \
|
||||
shadow-utils \
|
||||
apprise \
|
||||
jq \
|
||||
git \
|
||||
docker \
|
||||
&& microdnf clean all
|
||||
|
||||
RUN useradd --system --create-home olivetin -u 1000
|
||||
RUN useradd --system --create-home olivetin -u 1000
|
||||
|
||||
EXPOSE 1337/tcp
|
||||
EXPOSE 1337/tcp
|
||||
|
||||
COPY config.yaml /config
|
||||
COPY var/entities/* /config/entities/
|
||||
VOLUME /config
|
||||
|
||||
COPY OliveTin /usr/bin/OliveTin
|
||||
COPY webui /var/www/olivetin/
|
||||
COPY var/helper-actions/* /usr/bin/
|
||||
|
||||
USER olivetin
|
||||
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
FROM --platform=linux/arm64 registry.fedoraproject.org/fedora-minimal:36-aarch64
|
||||
FROM --platform=linux/arm64 registry.fedoraproject.org/fedora-minimal:40-aarch64
|
||||
|
||||
LABEL org.opencontainers.image.source https://github.com/OliveTin/OliveTin
|
||||
LABEL org.opencontainers.image.title=OliveTin
|
||||
|
||||
RUN mkdir -p /config /var/www/olivetin \
|
||||
RUN mkdir -p /config /config/entities/ /var/www/olivetin \
|
||||
&& \
|
||||
microdnf install -y --nodocs --noplugins --setopt=keepcache=0 --setopt=install_weak_deps=0 \
|
||||
microdnf install -y --nodocs --noplugins --setopt=keepcache=0 --setopt=install_weak_deps=0 \
|
||||
iputils \
|
||||
openssh-clients \
|
||||
shadow-utils \
|
||||
openssh-clients
|
||||
apprise \
|
||||
jq \
|
||||
git \
|
||||
docker \
|
||||
&& microdnf clean all
|
||||
|
||||
RUN useradd --system --create-home olivetin -u 1000
|
||||
RUN useradd --system --create-home olivetin -u 1000
|
||||
|
||||
EXPOSE 1337/tcp
|
||||
EXPOSE 1337/tcp
|
||||
|
||||
COPY config.yaml /config
|
||||
COPY var/entities/* /config/entities/
|
||||
VOLUME /config
|
||||
|
||||
COPY OliveTin /usr/bin/OliveTin
|
||||
COPY webui /var/www/olivetin/
|
||||
COPY var/helper-actions/* /usr/bin/
|
||||
|
||||
USER olivetin
|
||||
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
FROM --platform=linux/armhfp registry.fedoraproject.org/fedora-minimal:36-armhfp
|
||||
|
||||
LABEL org.opencontainers.image.source https://github.com/OliveTin/OliveTin
|
||||
LABEL org.opencontainers.image.title=OliveTin
|
||||
|
||||
RUN mkdir -p /config /var/www/olivetin \
|
||||
RUN mkdir -p /config /config/entities /var/www/olivetin \
|
||||
&& \
|
||||
microdnf install -y --nodocs --noplugins --setopt=keepcache=0 --setopt=install_weak_deps=0 \
|
||||
microdnf install -y --nodocs --noplugins --setopt=keepcache=0 --setopt=install_weak_deps=0 \
|
||||
iputils \
|
||||
shadow-utils \
|
||||
openssh-clients
|
||||
shadow-utils \
|
||||
openssh-clients
|
||||
|
||||
RUN useradd --system --create-home olivetin -u 1000
|
||||
RUN useradd --system --create-home olivetin -u 1000
|
||||
|
||||
EXPOSE 1337/tcp
|
||||
EXPOSE 1337/tcp
|
||||
|
||||
COPY config.yaml /config
|
||||
COPY var/entities/* /config/entities/
|
||||
VOLUME /config
|
||||
|
||||
COPY OliveTin /usr/bin/OliveTin
|
||||
COPY webui /var/www/olivetin/
|
||||
COPY var/helper-actions/* /usr/bin/
|
||||
|
||||
USER olivetin
|
||||
|
||||
|
||||
8
Jenkinsfile
vendored
8
Jenkinsfile
vendored
@@ -14,7 +14,7 @@ pipeline {
|
||||
sh 'make go-tools'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
stage('Compile') {
|
||||
steps {
|
||||
withEnv(["PATH+GO=/root/go/bin/"]) {
|
||||
@@ -25,9 +25,9 @@ pipeline {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
stage ('Post-Compile') {
|
||||
parallel {
|
||||
parallel {
|
||||
stage('Codestyle') {
|
||||
steps {
|
||||
withEnv(["PATH+GO=/root/go/bin/"]) {
|
||||
@@ -45,6 +45,6 @@ pipeline {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
65
Makefile
65
Makefile
@@ -1,30 +1,48 @@
|
||||
compile: daemon-compile-x64-lin
|
||||
define delete-files
|
||||
python -c "import shutil;shutil.rmtree('$(1)', ignore_errors=True)"
|
||||
endef
|
||||
|
||||
daemon-compile-armhf:
|
||||
GOARCH=arm GOARM=6 go build -o OliveTin.armhf github.com/OliveTin/OliveTin/cmd/OliveTin
|
||||
compile: daemon-compile-currentenv
|
||||
|
||||
daemon-compile-x64-lin:
|
||||
GOOS=linux go build -o OliveTin github.com/OliveTin/OliveTin/cmd/OliveTin
|
||||
daemon-compile-currentenv:
|
||||
go build github.com/OliveTin/OliveTin/cmd/OliveTin
|
||||
|
||||
daemon-compile-armhf:
|
||||
go env -w GOARCH=arm GOARM=6
|
||||
go build -o OliveTin.armhf github.com/OliveTin/OliveTin/cmd/OliveTin
|
||||
go env -u GOARCH GOARM
|
||||
|
||||
daemon-compile-x64-lin:
|
||||
go env -w GOOS=linux
|
||||
go build -o OliveTin github.com/OliveTin/OliveTin/cmd/OliveTin
|
||||
go env -u GOOS
|
||||
|
||||
daemon-compile-x64-win:
|
||||
GOOS=windows GOARCH=amd64 go build -o OliveTin.exe github.com/OliveTin/OliveTin/cmd/OliveTin
|
||||
go env -w GOOS=windows GOARCH=amd64
|
||||
go build -o OliveTin.exe github.com/OliveTin/OliveTin/cmd/OliveTin
|
||||
go env -u GOOS GOARCH
|
||||
|
||||
daemon-compile: daemon-compile-armhf daemon-compile-x64-lin daemon-compile-x64-win
|
||||
|
||||
daemon-codestyle:
|
||||
go fmt ./...
|
||||
go vet ./...
|
||||
gocyclo -over 4 cmd internal
|
||||
gocyclo -over 4 cmd internal
|
||||
gocritic check ./...
|
||||
|
||||
daemon-unittests:
|
||||
mkdir -p reports
|
||||
$(call delete-files,reports)
|
||||
mkdir reports
|
||||
go test ./... -coverprofile reports/unittests.out
|
||||
go tool cover -html=reports/unittests.out -o reports/unittests.html
|
||||
|
||||
|
||||
it:
|
||||
cd integration-tests && make
|
||||
|
||||
githooks:
|
||||
cp -v .githooks/* .git/hooks/
|
||||
|
||||
git config --local core.hooksPath .githooks
|
||||
|
||||
go-tools:
|
||||
go install "github.com/bufbuild/buf/cmd/buf"
|
||||
go install "github.com/fzipp/gocyclo/cmd/gocyclo"
|
||||
@@ -37,7 +55,7 @@ go-tools:
|
||||
grpc: go-tools
|
||||
buf generate
|
||||
|
||||
dist: protoc
|
||||
dist: protoc
|
||||
|
||||
protoc:
|
||||
protoc --go_out=. --go-grpc_out=. --grpc-gateway_out=. -I .:/usr/include/ OliveTin.proto
|
||||
@@ -54,7 +72,7 @@ podman-container:
|
||||
integration-tests-docker-image:
|
||||
docker rm -f olivetin && docker rmi -f olivetin
|
||||
docker build -t olivetin:latest .
|
||||
docker create --name olivetin -p 1337:1337 -v `pwd`/integration-tests/configs/:/config/ olivetin
|
||||
docker create --name olivetin -p 1337:1337 -v `pwd`/integration-tests/configs/:/config/ olivetin
|
||||
|
||||
devrun: compile
|
||||
killall OliveTin || true
|
||||
@@ -63,11 +81,24 @@ devrun: compile
|
||||
devcontainer: compile podman-image podman-container
|
||||
|
||||
webui-codestyle:
|
||||
cd webui && npm install
|
||||
cd webui && ./node_modules/.bin/eslint main.js js/*
|
||||
cd webui && ./node_modules/.bin/stylelint style.css
|
||||
cd webui.dev && npm install
|
||||
cd webui.dev && npx eslint main.js js/*
|
||||
cd webui.dev && npx stylelint style.css
|
||||
|
||||
webui-dist:
|
||||
$(call delete-files,webui)
|
||||
$(call delete-files,webui.dev/dist)
|
||||
cd webui.dev && npm install
|
||||
cd webui.dev && npx parcel build --public-url "."
|
||||
python -c "import shutil;shutil.move('webui.dev/dist', 'webui')"
|
||||
python -c "import shutil;import glob;[shutil.copy(f, 'webui') for f in glob.glob('webui.dev/*.png')]"
|
||||
|
||||
clean:
|
||||
rm -rf dist OliveTin OliveTin.armhf OliveTin.exe reports gen
|
||||
$(call delete-files,dist)
|
||||
$(call delete-files,OliveTin)
|
||||
$(call delete-files,OliveTin.armhf)
|
||||
$(call delete-files,OliveTin.exe)
|
||||
$(call delete-files,reports)
|
||||
$(call delete-files,gen)
|
||||
|
||||
.PHONY: grpc
|
||||
.PHONY: grpc
|
||||
|
||||
195
OliveTin.proto
195
OliveTin.proto
@@ -3,25 +3,28 @@ syntax = "proto3";
|
||||
option go_package = "gen/grpc";
|
||||
|
||||
import "google/api/annotations.proto";
|
||||
import "google/api/httpbody.proto";
|
||||
|
||||
message Action {
|
||||
string id = 1;
|
||||
string title = 2;
|
||||
string icon = 3;
|
||||
bool canExec = 4;
|
||||
|
||||
bool can_exec = 4;
|
||||
repeated ActionArgument arguments = 5;
|
||||
string popup_on_start = 6;
|
||||
int32 order = 7;
|
||||
}
|
||||
|
||||
message ActionArgument {
|
||||
string name = 1;
|
||||
string title = 2;
|
||||
string title = 2;
|
||||
string type = 3;
|
||||
string defaultValue = 4;
|
||||
string default_value = 4;
|
||||
|
||||
repeated ActionArgumentChoice choices = 5;
|
||||
|
||||
string description = 6;
|
||||
map<string, string> suggestions = 7;
|
||||
}
|
||||
|
||||
message ActionArgumentChoice {
|
||||
@@ -37,16 +40,30 @@ message Entity {
|
||||
|
||||
message GetDashboardComponentsResponse {
|
||||
string title = 1;
|
||||
|
||||
repeated Action actions = 2;
|
||||
repeated Entity entities = 3;
|
||||
repeated DashboardComponent dashboards = 4;
|
||||
|
||||
string authenticated_user = 5;
|
||||
}
|
||||
|
||||
message GetDashboardComponentsRequest {}
|
||||
|
||||
message DashboardComponent {
|
||||
string title = 1;
|
||||
string type = 2;
|
||||
repeated DashboardComponent contents = 3;
|
||||
string icon = 4;
|
||||
string css_class = 5;
|
||||
}
|
||||
|
||||
message StartActionRequest {
|
||||
string actionName = 1;
|
||||
string action_id = 1;
|
||||
|
||||
repeated StartActionArgument arguments = 2;
|
||||
|
||||
string unique_tracking_id = 3;
|
||||
}
|
||||
|
||||
message StartActionArgument {
|
||||
@@ -55,22 +72,51 @@ message StartActionArgument {
|
||||
}
|
||||
|
||||
message StartActionResponse {
|
||||
LogEntry logEntry = 1;
|
||||
string execution_tracking_id = 2;
|
||||
}
|
||||
|
||||
message StartActionAndWaitRequest {
|
||||
string action_id = 1;
|
||||
}
|
||||
|
||||
message StartActionAndWaitResponse {
|
||||
LogEntry log_entry = 1;
|
||||
}
|
||||
|
||||
message StartActionByGetRequest {
|
||||
string action_id = 1;
|
||||
}
|
||||
|
||||
message StartActionByGetResponse {
|
||||
string execution_tracking_id = 2;
|
||||
}
|
||||
|
||||
message StartActionByGetAndWaitRequest {
|
||||
string action_id = 1;
|
||||
}
|
||||
|
||||
message StartActionByGetAndWaitResponse {
|
||||
LogEntry log_entry = 1;
|
||||
}
|
||||
|
||||
message GetLogsRequest{};
|
||||
|
||||
message LogEntry {
|
||||
string datetime = 1;
|
||||
string actionTitle = 2;
|
||||
string stdout = 3;
|
||||
string stderr = 4;
|
||||
bool timedOut = 5;
|
||||
int32 exitCode = 6;
|
||||
string datetime_started = 1;
|
||||
string action_title = 2;
|
||||
string output = 3;
|
||||
bool timed_out = 5;
|
||||
int32 exit_code = 6;
|
||||
string user = 7;
|
||||
string userClass = 8;
|
||||
string actionIcon = 9;
|
||||
string user_class = 8;
|
||||
string action_icon = 9;
|
||||
repeated string tags = 10;
|
||||
string execution_tracking_id = 11;
|
||||
string datetime_finished = 12;
|
||||
string action_id = 13;
|
||||
bool execution_started = 14;
|
||||
bool execution_finished = 15;
|
||||
bool blocked = 16;
|
||||
}
|
||||
|
||||
message GetLogsResponse {
|
||||
@@ -87,10 +133,27 @@ message ValidateArgumentTypeResponse {
|
||||
string description = 2;
|
||||
}
|
||||
|
||||
message WatchExecutionRequest {
|
||||
string execution_tracking_id = 1;
|
||||
}
|
||||
|
||||
message WatchExecutionUpdate {
|
||||
string update = 1;
|
||||
}
|
||||
|
||||
message ExecutionStatusRequest {
|
||||
string execution_tracking_id = 1;
|
||||
string action_id = 2;
|
||||
}
|
||||
|
||||
message ExecutionStatusResponse {
|
||||
LogEntry log_entry = 1;
|
||||
}
|
||||
|
||||
message WhoAmIRequest {}
|
||||
|
||||
message WhoAmIResponse {
|
||||
string authenticatedUser = 1;
|
||||
string authenticated_user = 1;
|
||||
}
|
||||
|
||||
message SosReportRequest {}
|
||||
@@ -99,7 +162,54 @@ message SosReportResponse {
|
||||
string alert = 1;
|
||||
}
|
||||
|
||||
service OliveTinApi {
|
||||
message DumpVarsRequest {}
|
||||
|
||||
message DumpVarsResponse {
|
||||
string alert = 1;
|
||||
map<string, string> contents = 2;
|
||||
}
|
||||
|
||||
message ActionEntityPair {
|
||||
string action_title = 1;
|
||||
string entity_prefix = 2;
|
||||
}
|
||||
|
||||
message DumpPublicIdActionMapRequest {}
|
||||
message DumpPublicIdActionMapResponse {
|
||||
string alert = 1;
|
||||
map<string, ActionEntityPair> contents = 2;
|
||||
}
|
||||
|
||||
message GetReadyzRequest {}
|
||||
|
||||
message GetReadyzResponse {
|
||||
string status = 1;
|
||||
}
|
||||
|
||||
message EventOutputChunk {
|
||||
string execution_tracking_id = 1;
|
||||
|
||||
string output = 2;
|
||||
}
|
||||
|
||||
message EventEntityChanged {}
|
||||
message EventConfigChanged {}
|
||||
message EventExecutionFinished {
|
||||
LogEntry log_entry = 1;
|
||||
}
|
||||
|
||||
message KillActionRequest {
|
||||
string execution_tracking_id = 1;
|
||||
}
|
||||
|
||||
message KillActionResponse {
|
||||
string execution_tracking_id = 1;
|
||||
bool killed = 2;
|
||||
bool already_completed = 3;
|
||||
bool found = 4;
|
||||
}
|
||||
|
||||
service OliveTinApiService {
|
||||
rpc GetDashboardComponents(GetDashboardComponentsRequest) returns (GetDashboardComponentsResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/api/GetDashboardComponents"
|
||||
@@ -113,6 +223,39 @@ service OliveTinApi {
|
||||
};
|
||||
}
|
||||
|
||||
rpc StartActionAndWait(StartActionAndWaitRequest) returns (StartActionAndWaitResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/StartActionAndWait"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
rpc StartActionByGet(StartActionByGetRequest) returns (StartActionByGetResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/api/StartActionByGet/{action_id}"
|
||||
};
|
||||
}
|
||||
|
||||
rpc StartActionByGetAndWait(StartActionByGetAndWaitRequest) returns (StartActionByGetAndWaitResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/api/StartActionByGetAndWait/{action_id}"
|
||||
};
|
||||
}
|
||||
|
||||
rpc KillAction(KillActionRequest) returns (KillActionResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/KillAction"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
rpc ExecutionStatus(ExecutionStatusRequest) returns (ExecutionStatusResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/ExecutionStatus"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
rpc GetLogs(GetLogsRequest) returns (GetLogsResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/api/GetLogs"
|
||||
@@ -132,9 +275,27 @@ service OliveTinApi {
|
||||
};
|
||||
}
|
||||
|
||||
rpc SosReport(SosReportRequest) returns (SosReportResponse) {
|
||||
rpc SosReport(SosReportRequest) returns (google.api.HttpBody) {
|
||||
option (google.api.http) = {
|
||||
get: "/api/sosreport"
|
||||
};
|
||||
}
|
||||
|
||||
rpc DumpVars(DumpVarsRequest) returns (DumpVarsResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/api/DumpVars"
|
||||
};
|
||||
}
|
||||
|
||||
rpc DumpPublicIdActionMap(DumpPublicIdActionMapRequest) returns (DumpPublicIdActionMapResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/api/DumpActionMap"
|
||||
};
|
||||
}
|
||||
|
||||
rpc GetReadyz(GetReadyzRequest) returns (GetReadyzResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/api/readyz"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
52
README.md
52
README.md
@@ -1,8 +1,8 @@
|
||||
# OliveTin
|
||||
|
||||
<img alt = "project logo" src = "https://github.com/OliveTin/OliveTin/blob/main/webui/OliveTinLogo.png" align = "right" width = "160px" />
|
||||
<img alt = "project logo" src = "https://github.com/OliveTin/OliveTin/blob/main/webui.dev/OliveTinLogo.png" align = "right" width = "160px" />
|
||||
|
||||
OliveTin gives **safe** and **simple** access to predefined shell commands from a web interface.
|
||||
OliveTin gives **safe** and **simple** access to predefined shell commands from a web interface.
|
||||
|
||||
[](https://discord.gg/jhYWWpNJ3v)
|
||||
[](https://github.com/awesome-selfhosted/awesome-selfhosted#automation)
|
||||
@@ -11,7 +11,10 @@ OliveTin gives **safe** and **simple** access to predefined shell commands from
|
||||
[](https://goreportcard.com/report/github.com/OliveTin/OliveTin)
|
||||
[](https://github.com/OliveTin/OliveTin/actions/workflows/build-snapshot.yml)
|
||||
|
||||
## Use cases
|
||||
<img alt = "screenshot" src = "https://github.com/OliveTin/OliveTin/blob/main/var/marketing/screenshotDesktop.png" />
|
||||
<a href = "#screenshots">More screenshots below</a>
|
||||
|
||||
## Use cases
|
||||
|
||||
**Safely** give access to commands, for less technical people;
|
||||
|
||||
@@ -22,22 +25,22 @@ OliveTin gives **safe** and **simple** access to predefined shell commands from
|
||||
**Simplify** complex commands, make them accessible and repeatable;
|
||||
|
||||
* eg: Expose complex commands on touchscreen tablets stuck on walls around your house. `wake-on-lan aa:bb:cc:11:22:33`
|
||||
* eg: Run long running on your servers from your cell phone. `dnf update -y`
|
||||
* eg: Run long-lived commands on your servers from your cell phone. `dnf update -y`
|
||||
* eg: Define complex commands with lots of preset arguments, and turn a few arguments into dropdown select boxes. `docker rm {{ container }} && docker create {{ container }} && docker start {{ container }}`
|
||||
|
||||
[Join the community on Discord](https://discord.gg/jhYWWpNJ3v) to talk with other users about use cases, or to ask for support in getting started.
|
||||
|
||||
## YouTube demo video (6 mins)
|
||||
## YouTube demo video
|
||||
|
||||
[](https://www.youtube.com/watch?v=Ej6NM9rmZtk)
|
||||
[](https://www.youtube.com/watch?v=UBgOfNrzId4)
|
||||
|
||||
## Features
|
||||
|
||||
* **Responsive, touch-friendly UI** - great for tablets and mobile
|
||||
* **Super simple config in YAML** - because if it's not YAML now-a-days, it's not "cloud native" :-)
|
||||
* **Super simple config in YAML** - because if it's not YAML now-a-days, it's not "cloud native" :-)
|
||||
* **Dark mode** - for those of you that roll that way.
|
||||
* **Accessible** - passes all the accessibility checks in Firefox, and issues with accessibility are taken seriously.
|
||||
* **Container** - available for quickly testing and getting it up and running, great for the selfhosted community.
|
||||
* **Accessible** - passes all the accessibility checks in Firefox, and issues with accessibility are taken seriously.
|
||||
* **Container** - available for quickly testing and getting it up and running, great for the selfhosted community.
|
||||
* **Integrate with anything** - OliveTin just runs Linux shell commands, so theoretially you could integrate with a bunch of stuff just by using curl, ping, etc. However, writing your own shell scripts is a great way to extend OliveTin.
|
||||
* **Lightweight on resources** - uses only a few MB of RAM and barely any CPU. Written in Go, with a web interface written as a modern, responsive, Single Page App that uses the REST/gRPC API.
|
||||
* **Good amount of unit tests and style checks** - helps potential contributors be consistent, and helps with maintainability.
|
||||
@@ -46,19 +49,25 @@ OliveTin gives **safe** and **simple** access to predefined shell commands from
|
||||
|
||||
Desktop web browser;
|
||||
|
||||

|
||||
<p align = "center">
|
||||
<img alt = "screenshot" src = "https://github.com/OliveTin/OliveTin/blob/main/var/marketing/screenshotDesktop.png" />
|
||||
</p>
|
||||
|
||||
Desktop web browser (dark mode);
|
||||
Desktop web browser (dark mode);
|
||||
|
||||

|
||||
<p align = "center">
|
||||
<img alt = "screenshot" src = "https://github.com/OliveTin/OliveTin/blob/main/var/marketing/screenshotDesktopDark.png" />
|
||||
</p>
|
||||
|
||||
Mobile screen size (responsive layout);
|
||||
Mobile screen size (responsive layout);
|
||||
|
||||

|
||||
<p align = "center">
|
||||
<img alt = "screenshot" src = "https://github.com/OliveTin/OliveTin/blob/main/var/marketing/screenshotMobile.png" style = "height: 700px;" />
|
||||
</p>
|
||||
|
||||
## Documentation
|
||||
|
||||
All documentation can be found at http://docs.olivetin.app . This includes installation and usage guide, etc.
|
||||
All documentation can be found at http://docs.olivetin.app . This includes installation and usage guide, etc.
|
||||
|
||||
### Quickstart reference for `config.yaml`
|
||||
|
||||
@@ -75,19 +84,19 @@ Put this `config.yaml` in `/etc/OliveTin/` if you're running a standard service,
|
||||
|
||||
```yaml
|
||||
# Listen on all addresses available, port 1337
|
||||
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||
|
||||
# Choose from INFO (default), WARN and DEBUG
|
||||
logLevel: "INFO"
|
||||
|
||||
# Actions (buttons) to show up on the WebUI:
|
||||
actions:
|
||||
# Docs: https://docs.olivetin.app/action-container-control.html
|
||||
actions:
|
||||
# Docs: https://docs.olivetin.app/action-container-control.html
|
||||
- title: Restart Plex
|
||||
icon: restart
|
||||
shell: docker restart plex
|
||||
|
||||
# This will send 1 ping
|
||||
|
||||
# This will send 1 ping
|
||||
# Docs: https://docs.olivetin.app/action-ping.html
|
||||
- title: Ping host
|
||||
shell: ping {{ host }} -c {{ count }}
|
||||
@@ -102,7 +111,7 @@ actions:
|
||||
title: Count
|
||||
type: int
|
||||
default: 1
|
||||
|
||||
|
||||
# Restart http on host "webserver1"
|
||||
# Docs: https://docs.olivetin.app/action-ssh.html
|
||||
- title: restart httpd
|
||||
@@ -111,4 +120,3 @@ actions:
|
||||
```
|
||||
|
||||
A full example config can be found at in this repository - [config.yaml](https://github.com/OliveTin/OliveTin/blob/main/config.yaml).
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Currently, only the `main` branch is "supported".
|
||||
Currently, only the `main` branch is "supported".
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
@@ -10,4 +10,4 @@ Currently, only the `main` branch is "supported".
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please email `contact@jread.com` for responsible disclosure. Accepted issues will be made public once patched, and you will be given credit.
|
||||
Please email `contact@jread.com` for responsible disclosure. Accepted issues will be made public once patched, and you will be given credit.
|
||||
|
||||
@@ -17,4 +17,3 @@ plugins:
|
||||
|
||||
# - name: openapiv2
|
||||
# out: reports/openapiv2
|
||||
|
||||
|
||||
2
buf.yaml
2
buf.yaml
@@ -1,5 +1,5 @@
|
||||
version: v1
|
||||
deps:
|
||||
deps:
|
||||
- buf.build/googleapis/googleapis
|
||||
lint:
|
||||
use:
|
||||
|
||||
@@ -5,20 +5,23 @@ import (
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/OliveTin/OliveTin/internal/entityfiles"
|
||||
"github.com/OliveTin/OliveTin/internal/executor"
|
||||
grpcapi "github.com/OliveTin/OliveTin/internal/grpcapi"
|
||||
"github.com/OliveTin/OliveTin/internal/httpservers"
|
||||
"github.com/OliveTin/OliveTin/internal/installationinfo"
|
||||
"github.com/OliveTin/OliveTin/internal/oncalendarfile"
|
||||
"github.com/OliveTin/OliveTin/internal/oncron"
|
||||
"github.com/OliveTin/OliveTin/internal/onfileindir"
|
||||
"github.com/OliveTin/OliveTin/internal/onstartup"
|
||||
updatecheck "github.com/OliveTin/OliveTin/internal/updatecheck"
|
||||
|
||||
"github.com/OliveTin/OliveTin/internal/httpservers"
|
||||
"github.com/OliveTin/OliveTin/internal/websocket"
|
||||
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/spf13/viper"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -29,27 +32,78 @@ var (
|
||||
)
|
||||
|
||||
func init() {
|
||||
initLog()
|
||||
|
||||
initViperConfig(initCliFlags())
|
||||
|
||||
initCheckEnvironment()
|
||||
|
||||
initInstallationInfo()
|
||||
|
||||
log.Info("OliveTin initialization complete")
|
||||
}
|
||||
|
||||
func initLog() {
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
ForceQuote: true,
|
||||
DisableTimestamp: true,
|
||||
})
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"version": version,
|
||||
"commit": commit,
|
||||
"date": date,
|
||||
}).Info("OliveTin initializing")
|
||||
|
||||
log.SetLevel(log.DebugLevel) // Default to debug, to catch cfg issues
|
||||
// Use debug this early on to catch details about startup errors. The
|
||||
// default config will raise the log level later, if not set.
|
||||
log.SetLevel(log.DebugLevel) // Default to debug, to catch cfg issue
|
||||
}
|
||||
|
||||
func initCliFlags() string {
|
||||
var configDir string
|
||||
flag.StringVar(&configDir, "configdir", ".", "Config directory path")
|
||||
|
||||
var printVersion bool
|
||||
flag.BoolVar(&printVersion, "version", false, "Prints the version number and exits")
|
||||
flag.Parse()
|
||||
|
||||
// This log message should be the first log message OliveTin prints.
|
||||
if printVersion {
|
||||
logStartupMessage("OliveTin is just printing the startup message")
|
||||
os.Exit(1)
|
||||
} else {
|
||||
logStartupMessage("OliveTin initializing")
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"value": configDir,
|
||||
}).Debugf("Value of -configdir flag")
|
||||
|
||||
return configDir
|
||||
}
|
||||
|
||||
func getBasePort() int {
|
||||
var err error
|
||||
|
||||
defaultPort := 1337
|
||||
basePort := defaultPort
|
||||
|
||||
envPort := os.Getenv("PORT")
|
||||
|
||||
if envPort != "" {
|
||||
basePort, err = strconv.Atoi(os.Getenv("PORT"))
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Error converting port to int. %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if defaultPort != basePort {
|
||||
log.WithFields(log.Fields{
|
||||
"basePort": basePort,
|
||||
}).Debug("Base port")
|
||||
}
|
||||
|
||||
return basePort
|
||||
}
|
||||
|
||||
func initViperConfig(configDir string) {
|
||||
viper.AutomaticEnv()
|
||||
viper.SetConfigName("config.yaml")
|
||||
viper.SetConfigType("yaml")
|
||||
@@ -62,27 +116,37 @@ func init() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
cfg = config.DefaultConfig()
|
||||
cfg = config.DefaultConfigWithBasePort(getBasePort())
|
||||
|
||||
viper.WatchConfig()
|
||||
viper.OnConfigChange(func(e fsnotify.Event) {
|
||||
if e.Op == fsnotify.Write {
|
||||
log.Info("Config file changed:", e.String())
|
||||
|
||||
reloadConfig()
|
||||
config.Reload(cfg)
|
||||
}
|
||||
})
|
||||
|
||||
reloadConfig()
|
||||
|
||||
warnIfPuidGuid()
|
||||
config.Reload(cfg)
|
||||
}
|
||||
|
||||
func initInstallationInfo() {
|
||||
installationinfo.Config = cfg
|
||||
installationinfo.Build.Version = version
|
||||
installationinfo.Build.Commit = commit
|
||||
installationinfo.Build.Date = date
|
||||
}
|
||||
|
||||
log.Info("Init complete")
|
||||
func logStartupMessage(message string) {
|
||||
log.WithFields(log.Fields{
|
||||
"version": version,
|
||||
"commit": commit,
|
||||
"date": date,
|
||||
}).Info(message)
|
||||
}
|
||||
|
||||
func initCheckEnvironment() {
|
||||
warnIfPuidGuid()
|
||||
}
|
||||
|
||||
func warnIfPuidGuid() {
|
||||
@@ -91,30 +155,28 @@ func warnIfPuidGuid() {
|
||||
}
|
||||
}
|
||||
|
||||
func reloadConfig() {
|
||||
if err := viper.UnmarshalExact(&cfg); err != nil {
|
||||
log.Errorf("Config unmarshal error %+v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
cfg.Sanitize()
|
||||
}
|
||||
|
||||
func main() {
|
||||
configDir := path.Dir(viper.ConfigFileUsed())
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"configDir": configDir,
|
||||
"configDir": cfg.GetDir(),
|
||||
}).Infof("OliveTin started")
|
||||
|
||||
log.Debugf("Config: %+v", cfg)
|
||||
|
||||
executor := executor.DefaultExecutor()
|
||||
executor := executor.DefaultExecutor(cfg)
|
||||
executor.RebuildActionMap()
|
||||
executor.AddListener(websocket.ExecutionListener)
|
||||
config.AddListener(executor.RebuildActionMap)
|
||||
|
||||
go onstartup.Execute(cfg, executor)
|
||||
go oncron.Schedule(cfg, executor)
|
||||
go onfileindir.WatchFilesInDirectory(cfg, executor)
|
||||
go oncalendarfile.Schedule(cfg, executor)
|
||||
|
||||
go updatecheck.StartUpdateChecker(version, commit, cfg, configDir)
|
||||
entityfiles.AddListener(websocket.OnEntityChanged)
|
||||
entityfiles.AddListener(executor.RebuildActionMap)
|
||||
go entityfiles.SetupEntityFileWatchers(cfg)
|
||||
|
||||
go updatecheck.StartUpdateChecker(cfg)
|
||||
|
||||
go grpcapi.Start(cfg, executor)
|
||||
|
||||
|
||||
348
config.yaml
348
config.yaml
@@ -1,79 +1,303 @@
|
||||
# There is a built-in micro proxy that will host the webui and REST API all on
|
||||
# one port (this is called the "Single HTTP Frontend") and means you just need
|
||||
# one open port in the container/firewalls/etc.
|
||||
# There is a built-in micro proxy that will host the webui and REST API all on
|
||||
# one port (this is called the "Single HTTP Frontend") and means you just need
|
||||
# one open port in the container/firewalls/etc.
|
||||
#
|
||||
# Listen on all addresses available, port 1337
|
||||
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||
|
||||
# Choose from INFO (default), WARN and DEBUG
|
||||
logLevel: "INFO"
|
||||
|
||||
# Actions (buttons) to show up on the WebUI:
|
||||
actions:
|
||||
# This will run a simple script that you create.
|
||||
- title: Run backup script
|
||||
shell: /opt/backupScript.sh
|
||||
icon: backup
|
||||
# Checking for updates https://docs.olivetin.app/update-checks.html
|
||||
checkForUpdates: false
|
||||
|
||||
# This will send 1 ping (-c 1)
|
||||
# Docs: https://docs.olivetin.app/action-ping.html
|
||||
- title: Ping host
|
||||
shell: ping {{ host }} -c {{ count }}
|
||||
icon: ping
|
||||
arguments:
|
||||
- name: host
|
||||
title: host
|
||||
type: ascii_identifier
|
||||
default: example.com
|
||||
description: The host that you want to ping
|
||||
|
||||
- name: count
|
||||
title: Count
|
||||
type: int
|
||||
default: 1
|
||||
description: How many times to do you want to ping?
|
||||
|
||||
# Restart lightdm on host "server1"
|
||||
# Docs: https://docs.olivetin.app/action-ping.html
|
||||
- title: restart httpd
|
||||
icon: restart
|
||||
shell: ssh root@server1 'service httpd restart'
|
||||
|
||||
# OliveTin can run long-running jobs like Ansible playbooks.
|
||||
#
|
||||
# For such jobs, you will need to install ansible-playbook on the host where
|
||||
# you are running OliveTin, or in the container.
|
||||
# Actions are commands that are executed by OliveTin, and normally show up as
|
||||
# buttons on the WebUI.
|
||||
#
|
||||
# Docs: https://docs.olivetin.app/create-your-first-action.html
|
||||
actions:
|
||||
# This is the most simple action, it just runs the command and flashes the
|
||||
# button to indicate status.
|
||||
#
|
||||
# You probably want a much longer timeout as well (so that ansible completes).
|
||||
- title: "Run Ansible Playbook"
|
||||
icon: "🇦"
|
||||
shell: ansible-playbook -i /etc/hosts /root/myRepo/myPlaybook.yaml
|
||||
timeout: 120
|
||||
# If you are running OliveTin in a container remember to pass through the
|
||||
# docker socket! https://docs.olivetin.app/action-container-control.html
|
||||
- title: Ping the Internet
|
||||
shell: ping -c 3 1.1.1.1
|
||||
icon: ping
|
||||
popupOnStart: execution-dialog-stdout-only
|
||||
|
||||
# This uses `popupOnStart: execution-dialog-stdout-only` to simply show just
|
||||
# the command output.
|
||||
- title: Check disk space
|
||||
icon: disk
|
||||
shell: df -h /media
|
||||
popupOnStart: execution-dialog-stdout-only
|
||||
|
||||
# This uses `popupOnStart: execution-dialog` to show a dialog with more
|
||||
# information about the command that was run.
|
||||
- title: check dmesg logs
|
||||
shell: dmesg | tail
|
||||
icon: logs
|
||||
popupOnStart: execution-dialog
|
||||
|
||||
# This uses `popupOnStart: execution-button` to display a mini button that
|
||||
# links to the logs.
|
||||
- title: date
|
||||
shell: date
|
||||
timeout: 6
|
||||
icon: clock
|
||||
popupOnStart: execution-button
|
||||
|
||||
# You are not limited to operating system commands, and of course you can run
|
||||
# your own scripts. Here `maxConcurrent` stops the script running multiple
|
||||
# times in parallel. There is also a timeout that will kill the command if it
|
||||
# runs for too long.
|
||||
- title: Run backup script
|
||||
shell: /opt/backupScript.sh
|
||||
shellAfterCompleted: "apprise -t 'Notification: Backup script completed' -b 'The backup script completed with code {{ exitCode}}. The log is: \n {{ output }} '"
|
||||
maxConcurrent: 1
|
||||
timeout: 10
|
||||
icon: backup
|
||||
popupOnStart: execution-dialog
|
||||
|
||||
# When you want to prompt users for input, that is when you should use
|
||||
# `arguments` - this presents a popup dialog and asks for argument values.
|
||||
#
|
||||
# Docs: https://docs.olivetin.app/action-ping.html
|
||||
- title: Ping host
|
||||
shell: ping {{ host }} -c {{ count }}
|
||||
icon: ping
|
||||
timeout: 100
|
||||
popupOnStart: execution-dialog-stdout-only
|
||||
arguments:
|
||||
- name: host
|
||||
title: Host
|
||||
type: ascii_identifier
|
||||
default: example.com
|
||||
description: The host that you want to ping
|
||||
|
||||
- name: count
|
||||
title: Count
|
||||
type: int
|
||||
default: 3
|
||||
description: How many times to do you want to ping?
|
||||
|
||||
# OliveTin can control containers - docker is just a command line app.
|
||||
#
|
||||
#
|
||||
# However, if you are running in a container you will need to do some setup,
|
||||
# see the docs below.
|
||||
#
|
||||
# Docs: https://docs.olivetin.app/action-container-control.html
|
||||
- title: Restart Docker Container
|
||||
icon: restart
|
||||
shell: docker restart {{ container }}
|
||||
arguments:
|
||||
- name: container
|
||||
title: Container name
|
||||
choices:
|
||||
- value: plex
|
||||
- value: traefik
|
||||
- value: grafana
|
||||
- title: Restart Docker Container
|
||||
icon: restart
|
||||
shell: docker restart {{ container }}
|
||||
arguments:
|
||||
- name: container
|
||||
title: Container name
|
||||
choices:
|
||||
- value: plex
|
||||
- value: traefik
|
||||
- value: grafana
|
||||
|
||||
- title: Slow Script
|
||||
shell: sleep 3
|
||||
timeout: 5
|
||||
icon: "🥱"
|
||||
# There is a special `confirmation` argument to help against accidental clicks
|
||||
# on "dangerous" actions.
|
||||
#
|
||||
# Docs: https://docs.olivetin.app/confirmation.html
|
||||
- title: Delete old backups
|
||||
icon: ashtonished
|
||||
shell: rm -rf /opt/oldBackups/
|
||||
arguments:
|
||||
- type: confirmation
|
||||
title: Are you sure?!
|
||||
|
||||
- title: Broken Script (timeout)
|
||||
shell: sleep 5
|
||||
timeout: 5
|
||||
icon: "😪"
|
||||
# This is an action that runs a script included with OliveTin, that will
|
||||
# download themes. You will still need to set theme "themeName" in your config.
|
||||
#
|
||||
# Docs: https://docs.olivetin.app/themes.html
|
||||
- title: Get OliveTin Theme
|
||||
shell: olivetin-get-theme {{ themeGitRepo }} {{ themeFolderName }}
|
||||
icon: theme
|
||||
arguments:
|
||||
- name: themeGitRepo
|
||||
title: Theme's Git Repository
|
||||
description: Find new themes at https://olivetin.app/themes
|
||||
type: url
|
||||
|
||||
- name: themeFolderName
|
||||
title: Theme's Folder Name
|
||||
type: ascii_identifier
|
||||
|
||||
# Sometimes you want to run actions on other servers - don't overcomplicate
|
||||
# it, just use SSH! OliveTin includes a helper to make this easier, which is
|
||||
# entirely optional. You can also setup SSH manually.
|
||||
#
|
||||
# Docs: https://docs.olivetin.app/action-ssh-easy.html
|
||||
# Docs: https://docs.olivetin.app/action-ssh.html
|
||||
- title: "Setup easy SSH"
|
||||
icon: ssh
|
||||
shell: olivetin-setup-easy-ssh
|
||||
popupOnStart: execution-dialog
|
||||
|
||||
# Here's how to use SSH with the "easy" config, to restart a service on
|
||||
# another server.
|
||||
#
|
||||
# Docs: https://docs.olivetin.app/action-ssh-easy.html
|
||||
# Docs: https://docs.olivetin.app/action-service.html
|
||||
- title: Restart httpd on server1
|
||||
id: restart_httpd
|
||||
icon: restart
|
||||
timeout: 1
|
||||
shell: ssh -F /config/ssh/easy.cg root@server1 'service httpd restart'
|
||||
|
||||
# Lots of people use OliveTin to build web interfaces for their electronics
|
||||
# projects. It's best to install OliveTin as a native package (eg, .deb), and
|
||||
# then you can use either a python script or the `gpio` command.
|
||||
- title: Toggle GPIO light
|
||||
shell: gpioset gpiochip1 9=1
|
||||
icon: light
|
||||
|
||||
# There are several built-in shortcuts for the `icon` option, but you
|
||||
# can also just specify any HTML, this includes any unicode character,
|
||||
# or a <img = "..." /> link to a custom icon.
|
||||
#
|
||||
# Docs: https://docs.olivetin.app/icons.html
|
||||
#
|
||||
# Lots of people use OliveTin to easily execute ansible-playbooks. You
|
||||
# probably want a much longer timeout as well (so that ansible completes).
|
||||
#
|
||||
# Docs: https://docs.olivetin.app/ansible-playbook.html
|
||||
- title: "Run Automation Playbook"
|
||||
icon: '🤖'
|
||||
shell: ansible-playbook -i /etc/hosts /root/myRepo/myPlaybook.yaml
|
||||
timeout: 120
|
||||
|
||||
# The following actions are "dummy" actions, used in a Dashboard. As long as
|
||||
# you have these referenced in a dashboard, they will not show up in the
|
||||
# `actions` view.
|
||||
- title: Ping hypervisor1
|
||||
shell: echo "hypervisor1 online"
|
||||
|
||||
- title: Ping hypervisor2
|
||||
shell: echo "hypervisor2 online"
|
||||
|
||||
- title: "{{ server.name }} Wake on Lan"
|
||||
shell: echo "Sending Wake on LAN to {{ server.hostname }}"
|
||||
entity: server
|
||||
|
||||
- title: "{{ server.name }} Power Off"
|
||||
shell: "echo 'Power Off Server: {{ server.hostname }}'"
|
||||
entity: server
|
||||
|
||||
- title: Ping All Servers
|
||||
shell: "echo 'Ping all servers'"
|
||||
icon: ping
|
||||
|
||||
- title: Start {{ container.Names }}
|
||||
icon: box
|
||||
shell: docker start {{ container.Names }}
|
||||
entity: container
|
||||
trigger: Update container entity file
|
||||
|
||||
- title: Stop {{ container.Names }}
|
||||
icon: box
|
||||
shell: docker stop {{ container.Names }}
|
||||
entity: container
|
||||
trigger: Update container entity file
|
||||
|
||||
# Lastly, you can hide actions from the web UI, this is useful for creating
|
||||
# background helpers that execute only on startup or a cron, for updating
|
||||
# entity files.
|
||||
|
||||
# - title: Update container entity file
|
||||
# shell: 'docker ps -a --format json > /etc/OliveTin/entities/containers.json'
|
||||
# hidden: true
|
||||
# execOnStartup: true
|
||||
# execOnCron: '*/1 * * * *'
|
||||
|
||||
# An entity is something that exists - a "thing", like a VM, or a Container
|
||||
# is an entity. OliveTin allows you to then dynamically generate actions based
|
||||
# around these entities.
|
||||
#
|
||||
# This is really useful if you want to generate wake on lan or poweroff actions
|
||||
# for `server` entities, for example.
|
||||
#
|
||||
# A very popular use case that entities were designed for was for `container`
|
||||
# entities - in a similar way you could generate `start`, `stop`, and `restart`
|
||||
# container actions.
|
||||
#
|
||||
# Entities are just loaded fome files on disk, OliveTin will also watch these
|
||||
# files for updates while OliveTin is running, and update entities.
|
||||
#
|
||||
# Entities can have properties defined in those files, and those can be used
|
||||
# in your configuration as variables. For example; `container.status`,
|
||||
# or `vm.hostname`.
|
||||
#
|
||||
# Docs: http://docs.olivetin.app/entities.html
|
||||
entities:
|
||||
# YAML files are the default expected format, so you can use .yml or .yaml,
|
||||
# or even .txt, as long as the file contains valid a valid yaml LIST, then it
|
||||
# will load properly.
|
||||
#
|
||||
# Docs: https://docs.olivetin.app/entities.html
|
||||
- file: entities/servers.yaml
|
||||
name: server
|
||||
|
||||
- file: entities/containers.json
|
||||
name: container
|
||||
|
||||
# Dashboards are a way of taking actions from the default "actions" view, and
|
||||
# organizing them into groups - either into folders, or fieldsets.
|
||||
#
|
||||
# The only way to properly use entities, are to use them with a `fieldset` on
|
||||
# a dashboard.
|
||||
dashboards:
|
||||
# Top level items are dashboards.
|
||||
- title: My Servers
|
||||
contents:
|
||||
- title: All Servers
|
||||
type: fieldset
|
||||
contents:
|
||||
# The contents of a dashboard will try to look for an action with a
|
||||
# matching title IF the `contents: ` property is empty.
|
||||
- title: Ping All Servers
|
||||
|
||||
# If you create an item with some "contents:", OliveTin will show that as
|
||||
# directory.
|
||||
- title: Hypervisors
|
||||
contents:
|
||||
- title: Ping hypervisor1
|
||||
- title: Ping hypervisor2
|
||||
|
||||
# If you specify `type: fieldset` and some `contents`, it will show your
|
||||
# actions grouped together without a folder.
|
||||
- type: fieldset
|
||||
entity: server
|
||||
title: 'Server: {{ server.hostname }}'
|
||||
contents:
|
||||
# By default OliveTin will look for an action with a matching title
|
||||
# and put it on the dashboard.
|
||||
#
|
||||
# Fieldsets also support `type: display`, which can display arbitary
|
||||
# text. This is useful for displaying things like a container's state.
|
||||
- type: display
|
||||
title: |
|
||||
Hostname: <strong>{{ server.name }}</strong>
|
||||
IP Address: <strong>{{ server.ip }}</strong>
|
||||
|
||||
# These are the actions (defined above) that we want on the dashboard.
|
||||
- title: '{{ server.name }} Wake on Lan'
|
||||
- title: '{{ server.name }} Power Off'
|
||||
|
||||
# This is the second dashboard.
|
||||
- title: My Containers
|
||||
contents:
|
||||
- title: 'Container {{ container.Names }} ({{ container.Image }})'
|
||||
entity: container
|
||||
type: fieldset
|
||||
contents:
|
||||
- type: display
|
||||
title: |
|
||||
{{ container.RunningFor }} <br /><br /><strong>{{ container.State }}</strong>
|
||||
|
||||
- title: 'Start {{ container.Names }}'
|
||||
- title: 'Stop {{ container.Names }}'
|
||||
|
||||
140
go.mod
140
go.mod
@@ -1,103 +1,133 @@
|
||||
module github.com/OliveTin/OliveTin
|
||||
|
||||
go 1.18
|
||||
go 1.21
|
||||
|
||||
toolchain go1.21.9
|
||||
|
||||
require (
|
||||
github.com/bufbuild/buf v1.15.1
|
||||
github.com/MicahParks/keyfunc/v3 v3.3.2
|
||||
github.com/bufbuild/buf v1.30.1
|
||||
github.com/fsnotify/fsnotify v1.6.0
|
||||
github.com/fzipp/gocyclo v0.6.0
|
||||
github.com/go-critic/go-critic v0.7.0
|
||||
github.com/go-critic/go-critic v0.11.1
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.4.1
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1
|
||||
github.com/prometheus/client_golang v1.19.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/viper v1.15.0
|
||||
github.com/stretchr/testify v1.8.2
|
||||
golang.org/x/exp v0.0.0-20230307190834-24139beb5833
|
||||
google.golang.org/grpc v1.53.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240325203815-454cdb8f5daa
|
||||
google.golang.org/grpc v1.62.1
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0
|
||||
google.golang.org/protobuf v1.30.0
|
||||
google.golang.org/protobuf v1.33.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.33.0-20240221180331-f05a6f4403ce.1 // indirect
|
||||
connectrpc.com/connect v1.16.0 // indirect
|
||||
connectrpc.com/otelconnect v0.7.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.0 // indirect
|
||||
github.com/bufbuild/connect-go v1.5.2 // indirect
|
||||
github.com/bufbuild/protocompile v0.5.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/cristalhq/acmd v0.11.1 // indirect
|
||||
github.com/MicahParks/jwkset v0.5.17 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bufbuild/protocompile v0.9.0 // indirect
|
||||
github.com/bufbuild/protovalidate-go v0.6.0 // indirect
|
||||
github.com/bufbuild/protoyaml-go v0.1.8 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
||||
github.com/cristalhq/acmd v0.11.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/docker/cli v23.0.1+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||
github.com/docker/docker v23.0.1+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.7.0 // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/cli v26.0.0+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker v26.1.5+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.8.1 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/felixge/fgprof v0.9.3 // indirect
|
||||
github.com/go-chi/chi/v5 v5.0.8 // indirect
|
||||
github.com/go-logr/logr v1.2.3 // indirect
|
||||
github.com/felixge/fgprof v0.9.4 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-chi/chi/v5 v5.0.12 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-toolsmith/astcast v1.1.0 // indirect
|
||||
github.com/go-toolsmith/astcopy v1.1.0 // indirect
|
||||
github.com/go-toolsmith/astequal v1.1.0 // indirect
|
||||
github.com/go-toolsmith/astequal v1.2.0 // indirect
|
||||
github.com/go-toolsmith/astfmt v1.1.0 // indirect
|
||||
github.com/go-toolsmith/astp v1.1.0 // indirect
|
||||
github.com/go-toolsmith/pkgload v1.2.2 // indirect
|
||||
github.com/go-toolsmith/strparse v1.1.0 // indirect
|
||||
github.com/go-toolsmith/typep v1.1.0 // indirect
|
||||
github.com/gofrs/flock v0.8.1 // indirect
|
||||
github.com/gofrs/uuid/v5 v5.0.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/glog v1.0.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/go-containerregistry v0.13.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20230228050547-1710fef4ab10 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/cel-go v0.20.1 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/go-containerregistry v0.19.1 // indirect
|
||||
github.com/google/pprof v0.0.0-20240327155427-868f304927ed // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84 // indirect
|
||||
github.com/klauspost/compress v1.16.0 // indirect
|
||||
github.com/klauspost/pgzip v1.2.5 // indirect
|
||||
github.com/jdx/go-netrc v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.17.7 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0-rc2 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pkg/profile v1.7.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/quasilyte/go-ruleguard v0.3.19 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/quasilyte/go-ruleguard v0.4.2 // indirect
|
||||
github.com/quasilyte/gogrep v0.5.0 // indirect
|
||||
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
|
||||
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
|
||||
github.com/rs/cors v1.8.3 // indirect
|
||||
github.com/rs/cors v1.11.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/spf13/afero v1.9.3 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/cobra v1.6.1 // indirect
|
||||
github.com/spf13/cobra v1.8.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.0 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
go.opentelemetry.io/otel v1.14.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.14.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.14.0 // indirect
|
||||
go.uber.org/atomic v1.10.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
go.uber.org/zap v1.24.0 // indirect
|
||||
golang.org/x/crypto v0.7.0 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20230213192124-5e25df0256eb // indirect
|
||||
golang.org/x/mod v0.9.0 // indirect
|
||||
golang.org/x/net v0.8.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/term v0.6.0 // indirect
|
||||
golang.org/x/text v0.8.0 // indirect
|
||||
golang.org/x/tools v0.7.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230223222841-637eb2293923 // indirect
|
||||
github.com/vbatts/tar-split v0.11.5 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
||||
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.1.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.21.0 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20240222234643-814bf88cf225 // indirect
|
||||
golang.org/x/mod v0.16.0 // indirect
|
||||
golang.org/x/net v0.23.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/term v0.18.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.19.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
314
go.sum
314
go.sum
@@ -1,3 +1,5 @@
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.33.0-20240221180331-f05a6f4403ce.1 h1:0nWhrRcnkgw1kwJ7xibIO8bqfOA7pBzBjGCDBxIHch8=
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.33.0-20240221180331-f05a6f4403ce.1/go.mod h1:Tgn5bgL220vkFOI0KPStlcClPeOJzAv4uT+V8JXGUnw=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
@@ -35,47 +37,77 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
||||
connectrpc.com/connect v1.16.0 h1:rdtfQjZ0OyFkWPTegBNcH7cwquGAN1WzyJy80oFNibg=
|
||||
connectrpc.com/connect v1.16.0/go.mod h1:XpZAduBQUySsb4/KO5JffORVkDI4B6/EYPi7N8xpNZw=
|
||||
connectrpc.com/otelconnect v0.7.0 h1:ZH55ZZtcJOTKWWLy3qmL4Pam4RzRWBJFOqTPyAqCXkY=
|
||||
connectrpc.com/otelconnect v0.7.0/go.mod h1:Bt2ivBymHZHqxvo4HkJ0EwHuUzQN6k2l0oH+mp/8nwc=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
|
||||
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
|
||||
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
|
||||
github.com/bufbuild/buf v1.15.1 h1:v7sK2uMEsGX4Z2hvu+xiMheH3C3AKBGfxPBgdUZYDQ8=
|
||||
github.com/bufbuild/buf v1.15.1/go.mod h1:TQeGKam1QMfHy/xsSnnMpxN3JK5HBb6aNvZj4m52gkE=
|
||||
github.com/bufbuild/connect-go v1.5.2 h1:G4EZd5gF1U1ZhhbVJXplbuUnfKpBZ5j5izqIwu2g2W8=
|
||||
github.com/bufbuild/connect-go v1.5.2/go.mod h1:GmMJYR6orFqD0Y6ZgX8pwQ8j9baizDrIQMm1/a6LnHk=
|
||||
github.com/bufbuild/protocompile v0.5.1 h1:mixz5lJX4Hiz4FpqFREJHIXLfaLBntfaJv1h+/jS+Qg=
|
||||
github.com/bufbuild/protocompile v0.5.1/go.mod h1:G5iLmavmF4NsYtpZFvE3B/zFch2GIY8+wjsYLR/lc40=
|
||||
github.com/MicahParks/jwkset v0.5.17 h1:DrcwyKwSP5adD0G2XJTvDulnWXjD6gbjROMgMXDbkKA=
|
||||
github.com/MicahParks/jwkset v0.5.17/go.mod h1:q8ptTGn/Z9c4MwbcfeCDssADeVQb3Pk7PnVxrvi+2QY=
|
||||
github.com/MicahParks/keyfunc/v3 v3.3.2 h1:YTtwc4dxalBZKFqHhqctBWN6VhbLdGhywmne9u5RQVM=
|
||||
github.com/MicahParks/keyfunc/v3 v3.3.2/go.mod h1:GJBeEjnv25OnD9y2OYQa7ELU6gYahEMBNXINZb+qm34=
|
||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bufbuild/buf v1.30.1 h1:QFtanwsXodoGFAwzXFXGXpzBkb7N2u8ZDyA3jWB4Pbs=
|
||||
github.com/bufbuild/buf v1.30.1/go.mod h1:7W8DJnj76wQa55EA3z2CmDxS0/nsHh8FqtE00dyDAdA=
|
||||
github.com/bufbuild/protocompile v0.9.0 h1:DI8qLG5PEO0Mu1Oj51YFPqtx6I3qYXUAhJVJ/IzAVl0=
|
||||
github.com/bufbuild/protocompile v0.9.0/go.mod h1:s89m1O8CqSYpyE/YaSGtg1r1YFMF5nLTwh4vlj6O444=
|
||||
github.com/bufbuild/protovalidate-go v0.6.0 h1:Jgs1kFuZ2LHvvdj8SpCLA1W/+pXS8QSM3F/E2l3InPY=
|
||||
github.com/bufbuild/protovalidate-go v0.6.0/go.mod h1:1LamgoYHZ2NdIQH0XGczGTc6Z8YrTHjcJVmiBaar4t4=
|
||||
github.com/bufbuild/protoyaml-go v0.1.8 h1:X9QDLfl9uEllh4gsXUGqPanZYCOKzd92uniRtW2OnAQ=
|
||||
github.com/bufbuild/protoyaml-go v0.1.8/go.mod h1:R8vE2+l49bSiIExP4VJpxOXleHE+FDzZ6HVxr3cYunw=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||
github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
|
||||
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.12.1 h1:+7nYmHJb0tEkcRaAW+MHqoKaJYZmkikupxCqVtmPuY0=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/cristalhq/acmd v0.11.1 h1:DJ4fh2Pv0nPKmqT646IU/0Vh5FNdGblxvF+3/W3NAUI=
|
||||
github.com/cristalhq/acmd v0.11.1/go.mod h1:LG5oa43pE/BbxtfMoImHCQN++0Su7dzipdgBjMCBVDQ=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/cristalhq/acmd v0.11.2 h1:ITIWtBRiYbmzk+i8xQgH2RzfCVMII+dOd0CtGWVIhaU=
|
||||
github.com/cristalhq/acmd v0.11.2/go.mod h1:LG5oa43pE/BbxtfMoImHCQN++0Su7dzipdgBjMCBVDQ=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docker/cli v23.0.1+incompatible h1:LRyWITpGzl2C9e9uGxzisptnxAn1zfZKXy13Ul2Q5oM=
|
||||
github.com/docker/cli v23.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
|
||||
github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v23.0.1+incompatible h1:vjgvJZxprTTE1A37nm+CLNAdwu6xZekyoiVlUZEINcY=
|
||||
github.com/docker/docker v23.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
|
||||
github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/cli v26.0.0+incompatible h1:90BKrx1a1HKYpSnnBFR6AgDq/FqkHxwlUyzJVPxD30I=
|
||||
github.com/docker/cli v26.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v26.1.5+incompatible h1:NEAxTwEjxV6VbBMBoGG3zPqbiJosIApZjxlbrG9q3/g=
|
||||
github.com/docker/docker v26.1.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo=
|
||||
github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
@@ -84,23 +116,29 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
|
||||
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
|
||||
github.com/felixge/fgprof v0.9.4 h1:ocDNwMFlnA0NU0zSB3I52xkO4sFXk80VK9lXjLClu88=
|
||||
github.com/felixge/fgprof v0.9.4/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
|
||||
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
|
||||
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
|
||||
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
|
||||
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-critic/go-critic v0.7.0 h1:tqbKzB8pqi0NsRZ+1pyU4aweAF7A7QN0Pi4Q02+rYnQ=
|
||||
github.com/go-critic/go-critic v0.7.0/go.mod h1:moYzd7GdVXE2C2hYTwd7h0CPcqlUeclsyBRwMa38v64=
|
||||
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
||||
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-critic/go-critic v0.11.1 h1:/zBseUSUMytnRqxjlsYNbDDxpu3R2yH8oLXo/FOE8b8=
|
||||
github.com/go-critic/go-critic v0.11.1/go.mod h1:aZVQR7+gazH6aDEQx4356SD7d8ez8MipYjXbEl5JAKA=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
|
||||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8=
|
||||
@@ -108,8 +146,9 @@ github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4
|
||||
github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s=
|
||||
github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw=
|
||||
github.com/go-toolsmith/astequal v1.0.3/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4=
|
||||
github.com/go-toolsmith/astequal v1.1.0 h1:kHKm1AWqClYn15R0K1KKE4RG614D46n+nqUQ06E1dTw=
|
||||
github.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ=
|
||||
github.com/go-toolsmith/astequal v1.2.0 h1:3Fs3CYZ1k9Vo4FzFhwwewC3CHISHDnVUPC4x0bI2+Cw=
|
||||
github.com/go-toolsmith/astequal v1.2.0/go.mod h1:c8NZ3+kSFtFY/8lPso4v8LuJjdJiUFVnSuU3s0qrrDY=
|
||||
github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco=
|
||||
github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4=
|
||||
github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA=
|
||||
@@ -121,6 +160,9 @@ github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQi
|
||||
github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ=
|
||||
github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus=
|
||||
github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig=
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
|
||||
@@ -129,9 +171,9 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
|
||||
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@@ -157,10 +199,12 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84=
|
||||
github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
@@ -172,9 +216,10 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-containerregistry v0.13.0 h1:y1C7Z3e149OJbOPDBxLYR8ITPz8dTKqQwjErKVHJC8k=
|
||||
github.com/google/go-containerregistry v0.13.0/go.mod h1:J9FQ+eSS4a1aC2GNZxvNpbWhgp0487v+cgiilB4FqDo=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-containerregistry v0.19.1 h1:yMQ62Al6/V0Z7CqIrrS1iYoA5/oQCm88DeNujc7C1KY=
|
||||
github.com/google/go-containerregistry v0.19.1/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
@@ -189,17 +234,20 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe
|
||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
|
||||
github.com/google/pprof v0.0.0-20230228050547-1710fef4ab10 h1:CqYfpuYIjnlNxM3msdyPRKabhXZWbKjf3Q8BWROFBso=
|
||||
github.com/google/pprof v0.0.0-20230228050547-1710fef4ab10/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk=
|
||||
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||
github.com/google/pprof v0.0.0-20240327155427-868f304927ed h1:n8QtJTrwsv3P7dNxPaMeNkMcxvUpqocsHLr8iDLGlQI=
|
||||
github.com/google/pprof v0.0.0-20240327155427-868f304927ed/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 h1:gDLXvp5S9izjldquuoAhDzccbskOL6tDC5jMSyx3zxE=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2/go.mod h1:7pdNwVWBBHGiCxa9lAszqCJMbfTISJ7oMftp8+UGV08=
|
||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
@@ -207,45 +255,53 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84 h1:2uT3aivO7NVpUPGcQX7RbHijHMyWix/yCnIrCWc+5co=
|
||||
github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84/go.mod h1:Zi/ZFkEqFHTm7qkjyNJjaWH4LQA9LQhGJyF0lTYGpxw=
|
||||
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
|
||||
github.com/jdx/go-netrc v1.0.0 h1:QbLMLyCZGj0NA8glAhxUpf1zDg6cxnWgMBbjq40W0gQ=
|
||||
github.com/jdx/go-netrc v1.0.0/go.mod h1:Gh9eFQJnoTNIRHXl2j5bJXA1u84hQWJWgGh569zF3v8=
|
||||
github.com/jhump/protoreflect v1.15.6 h1:WMYJbw2Wo+KOWwZFvgY0jMoVHM6i4XIvRs2RcBj5VmI=
|
||||
github.com/jhump/protoreflect v1.15.6/go.mod h1:jCHoyYQIJnaabEYnbGwyo9hUqfyUMTbJw/tAut5t97E=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
|
||||
github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
|
||||
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
|
||||
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA=
|
||||
github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
|
||||
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
|
||||
@@ -253,9 +309,17 @@ github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDj
|
||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/quasilyte/go-ruleguard v0.3.19 h1:tfMnabXle/HzOb5Xe9CUZYWXKfkS1KwRmZyPmD9nVcc=
|
||||
github.com/quasilyte/go-ruleguard v0.3.19/go.mod h1:lHSn69Scl48I7Gt9cX3VrbsZYvYiBYszZOZW4A+oTEw=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/quasilyte/go-ruleguard v0.4.2 h1:htXcXDK6/rO12kiTHKfHuqR4kr3Y4M0J0rOL6CH/BYs=
|
||||
github.com/quasilyte/go-ruleguard v0.4.2/go.mod h1:GJLgqsLeo4qgavUoL8JeGFNS7qcisx3awV/w9eWTmNI=
|
||||
github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo=
|
||||
github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng=
|
||||
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU=
|
||||
@@ -265,25 +329,28 @@ github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
||||
github.com/rs/cors v1.8.3 h1:O+qNyWn7Z+F9M0ILBHgMVPuB1xTOucVd5gtaYyXBpRo=
|
||||
github.com/rs/cors v1.8.3/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po=
|
||||
github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
|
||||
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
||||
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
|
||||
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
|
||||
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
|
||||
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU=
|
||||
github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA=
|
||||
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
|
||||
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -294,11 +361,12 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
|
||||
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/vbatts/tar-split v0.11.2 h1:Via6XqJr0hceW4wff3QRzD5gAk/tatMw/4ZA7cTlIME=
|
||||
github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts=
|
||||
github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
@@ -309,19 +377,32 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM=
|
||||
go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU=
|
||||
go.opentelemetry.io/otel/sdk v1.14.0 h1:PDCppFRDq8A1jL9v6KMI6dYesaq+DFcDZvjsoGvxGzY=
|
||||
go.opentelemetry.io/otel/sdk v1.14.0/go.mod h1:bwIC5TjrNG6QDCHNWvW4HLHtUQ4I+VQDsnjhvyZCALM=
|
||||
go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M=
|
||||
go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8=
|
||||
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
|
||||
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
|
||||
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
|
||||
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
|
||||
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM=
|
||||
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
|
||||
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.19.0 h1:EJoTO5qysMsYCa+w4UghwFV/ptQgqSL/8Ni+hx+8i1k=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.19.0/go.mod h1:XjG0jQyFJrv2PbMvwND7LwCEhsJzCzV5210euduKcKY=
|
||||
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI=
|
||||
go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
@@ -329,8 +410,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -341,12 +422,12 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20230307190834-24139beb5833 h1:SChBja7BCQewoTAU7IgvucQKMIXrEpFxNMs0spT3/5s=
|
||||
golang.org/x/exp v0.0.0-20230307190834-24139beb5833/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw=
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
|
||||
golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20230213192124-5e25df0256eb h1:WGs/bGIWYyAY5PVgGGMXqGGCxSJz4fpoUExb/vgqNCU=
|
||||
golang.org/x/exp/typeparams v0.0.0-20230213192124-5e25df0256eb/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20240222234643-814bf88cf225 h1:BzKNaIRXh1bD+1557OcFIHlpYBiVbK4zEyn8zBHi1SE=
|
||||
golang.org/x/exp/typeparams v0.0.0-20240222234643-814bf88cf225/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -370,8 +451,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
|
||||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
|
||||
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -403,8 +484,8 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -424,8 +505,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -460,28 +541,31 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@@ -531,8 +615,8 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
|
||||
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
|
||||
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
|
||||
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -599,8 +683,10 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D
|
||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20230223222841-637eb2293923 h1:znp6mq/drrY+6khTAlJUDNFFcDGV2ENLYKpMq8SyCds=
|
||||
google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240325203815-454cdb8f5daa h1:Jt1XW5PaLXF1/ePZrznsh/aAUvI7Adfc3LY1dAKlzRs=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240325203815-454cdb8f5daa/go.mod h1:K4kfzHtI0kqWA79gecJarFtDn/Mls+GxQcg3Zox91Ac=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa h1:RBgMaUMP+6soRkik4VoN8ojR2nex2TqZwjSSogic+eo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
@@ -617,8 +703,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
|
||||
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
||||
google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
|
||||
google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 h1:rNBFJjBCOgVr9pWD7rs/knKL4FRTKgpZmsRfV214zcA=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0/go.mod h1:Dk1tviKTvMCz5tvh7t+fh94dhmQVHuCt2OzJB3CTW9Y=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
@@ -632,13 +718,12 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
@@ -647,6 +732,7 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
|
||||
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
||||
8
integration-tests/.eslintrc.yml
Normal file
8
integration-tests/.eslintrc.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
env:
|
||||
browser: true
|
||||
es2021: true
|
||||
extends: 'eslint:recommended'
|
||||
parserOptions:
|
||||
ecmaVersion: 12
|
||||
sourceType: module
|
||||
rules: {}
|
||||
3
integration-tests/.mocharc.yml
Normal file
3
integration-tests/.mocharc.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
require:
|
||||
- mochaSetup.mjs
|
||||
@@ -1,6 +1,23 @@
|
||||
cypress:
|
||||
npm install
|
||||
./cypressRun.sh "general"
|
||||
./cypressRun.sh "hiddenNav"
|
||||
default: test-install test-run
|
||||
|
||||
.PHONY: cypress container
|
||||
test-install:
|
||||
npm install --no-fund
|
||||
|
||||
test-run:
|
||||
npx mocha -t 10000
|
||||
|
||||
find-flakey-tests:
|
||||
echo "Running test-run infinately"
|
||||
sh -c "while make test-run; do :; done"
|
||||
|
||||
nginx:
|
||||
podman-compose up -d nginx
|
||||
|
||||
clean:
|
||||
podman-compose down
|
||||
|
||||
getsnapshot:
|
||||
rm -rf /opt/OliveTin-snapshot/*
|
||||
gh run download -D /opt/OliveTin-snapshot/
|
||||
|
||||
.PHONY: default
|
||||
|
||||
@@ -1 +1,14 @@
|
||||
# OliveTin-integration-tests
|
||||
# OliveTin-integration-tests
|
||||
|
||||
## GitHub Actions (Ubuntu, Local Process)
|
||||
|
||||
- `mocha` is run with the default runner that starts and stops OliveTin as a local process (ie, localhost:1337).
|
||||
|
||||
## Running different configurations (Local Process, VM, Container)
|
||||
|
||||
- Get the snapshot you want to test `make getsnapshot`
|
||||
- To test against VMs:
|
||||
-- `export OLIVETIN_TEST_RUNNER=vm`
|
||||
-- `vagrant up fedora38` (or whatever distro you like defined in `Vagrantfile`)
|
||||
-- `. envVagrant.sh fedora38` to set the $IP and $PORT
|
||||
- `mocha`
|
||||
|
||||
39
integration-tests/Vagrantfile
vendored
39
integration-tests/Vagrantfile
vendored
@@ -2,34 +2,35 @@
|
||||
# (eg, snapshot builds on GitHub)
|
||||
|
||||
|
||||
Vagrant.configure("2") do |config|
|
||||
config.vm.box = "generic/centos8"
|
||||
config.vm.provision "shell", inline: "mkdir /etc/OliveTin && chmod o+w /etc/OliveTin/", privileged: true
|
||||
config.vm.provision "file", source: "configs/config.general.yaml/.", destination: "/etc/OliveTin/config.yaml"
|
||||
Vagrant.configure("2") do |config|
|
||||
config.vm.provision "shell", inline: "mkdir /etc/OliveTin && chmod o+w /etc/OliveTin/ && mkdir -p /opt/OliveTin-configs/ && chmod 0777 /opt/OliveTin-configs", privileged: true
|
||||
config.vm.provision "file", source: "configs/.", destination: "/opt/OliveTin-configs/"
|
||||
|
||||
config.vm.provider :libvirt do |libvirt|
|
||||
libvirt.management_network_device = 'virbr0'
|
||||
end
|
||||
|
||||
config.vm.define :f36 do |f36|
|
||||
f36.vm.box = "generic/fedora36"
|
||||
f36.vm.provision "file", source: "/opt/OliveTin-vagrant/linux_amd64_rpm/.", destination: "."
|
||||
f36.vm.provision "shell", inline: "rpm -U OliveTin* && systemctl enable --now OliveTin && systemctl disable --now firewalld"
|
||||
config.vm.define :stream9 do |i|
|
||||
i.vm.box = "centos/stream9"
|
||||
i.vm.provision "file", source: "/opt/OliveTin-snapshot/OliveTin_linux_amd64.rpm", destination: "$HOME/"
|
||||
i.vm.provision "shell", inline: "rpm -U OliveTin* && systemctl enable --now OliveTin && systemctl disable --now firewalld"
|
||||
end
|
||||
|
||||
config.vm.define :debian do |debian|
|
||||
debian.vm.box = "generic/debian10"
|
||||
debian.vm.provision "file", source: "/opt/OliveTin-vagrant/linux_amd64_deb/.", destination: "."
|
||||
debian.vm.provision "shell", inline: "dpkg --force-confold -i OliveTin* && systemctl enable --now OliveTin"
|
||||
config.vm.define :fedora38 do |i|
|
||||
i.vm.box = "generic/fedora38"
|
||||
i.vm.provision "file", source: "/opt/OliveTin-snapshot/OliveTin_linux_amd64.rpm", destination: "$HOME/"
|
||||
i.vm.provision "shell", inline: "rpm -U OliveTin* && systemctl enable --now OliveTin && systemctl disable --now firewalld"
|
||||
end
|
||||
|
||||
config.vm.define :ubuntu do |ubuntu|
|
||||
ubuntu.vm.box = "generic/ubuntu2110"
|
||||
ubuntu.vm.provision "file", source: "/opt/OliveTin-vagrant/linux_amd64_deb/.", destination: "."
|
||||
ubuntu.vm.provision "shell", inline: "dpkg --force-confold -i OliveTin* && systemctl enable --now OliveTin && systemctl disable --now firewalld"
|
||||
config.vm.define :debian12 do |i|
|
||||
i.vm.box = "debian/bookworm64"
|
||||
i.vm.provision "file", source: "/opt/OliveTin-snapshot/OliveTin_linux_amd64.deb", destination: "$HOME/"
|
||||
i.vm.provision "shell", inline: "dpkg --force-confold -i OliveTin* && systemctl enable --now OliveTin"
|
||||
end
|
||||
|
||||
# TODO
|
||||
#
|
||||
|
||||
config.vm.define :ubuntu2310 do |i|
|
||||
i.vm.box = "ubuntu/mantic64"
|
||||
i.vm.provision "file", source: "/opt/OliveTin-snapshot/OliveTin_linux_amd64.deb", destination: "$HOME/"
|
||||
i.vm.provision "shell", inline: "dpkg --force-confold -i OliveTin* && systemctl enable --now OliveTin && systemctl disable --now firewalld"
|
||||
end
|
||||
end
|
||||
|
||||
17
integration-tests/compose.yml
Normal file
17
integration-tests/compose.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
version: "3.8"
|
||||
services:
|
||||
nginx:
|
||||
container_name: nginx
|
||||
image: docker.io/nginx
|
||||
volumes:
|
||||
- ./proxies/nginx/:/etc/nginx/
|
||||
ports:
|
||||
- "8443:8443"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- otproxy
|
||||
|
||||
networks:
|
||||
otproxy:
|
||||
name: otproxy
|
||||
18
integration-tests/configs/entities/config.yaml
Normal file
18
integration-tests/configs/entities/config.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
#
|
||||
# Integration Test Config: Entities
|
||||
#
|
||||
|
||||
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||
|
||||
logLevel: "DEBUG"
|
||||
checkForUpdates: false
|
||||
|
||||
actions:
|
||||
- title: Ping {{ server.hostname }}
|
||||
shell: ping {{ server.hostname }}
|
||||
icon: ping
|
||||
entity: server
|
||||
|
||||
entities:
|
||||
- file: entities/servers.yaml
|
||||
name: server
|
||||
3
integration-tests/configs/entities/entities/servers.yaml
Normal file
3
integration-tests/configs/entities/entities/servers.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
- hostname: server1
|
||||
- hostname: server2
|
||||
- hostname: server3
|
||||
@@ -1,17 +1,17 @@
|
||||
#
|
||||
# Integration Test Config: General
|
||||
#
|
||||
#
|
||||
|
||||
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||
|
||||
logLevel: "DEBUG"
|
||||
checkForUpdates: false
|
||||
checkForUpdates: false
|
||||
|
||||
actions:
|
||||
actions:
|
||||
- title: Ping Google.com
|
||||
shell: ping google.com -c 1
|
||||
icon: ping
|
||||
|
||||
|
||||
- title: restart lightdm
|
||||
icon: poop
|
||||
shell: ssh root@overseer 'service lightdm restart'
|
||||
@@ -24,6 +24,13 @@ actions:
|
||||
shell: sleep 5
|
||||
icon: "😪"
|
||||
|
||||
- title: dir-popup
|
||||
shell: dir
|
||||
popupOnStart: execution-dialog-stdout-only
|
||||
|
||||
- title: cd-passive
|
||||
shell: cd
|
||||
|
||||
- title: "Run Ansible Playbook"
|
||||
icon: "🇦"
|
||||
shell: ansible-playbook -i /etc/hosts /root/myRepo/myPlaybook.yaml
|
||||
@@ -32,4 +39,3 @@ actions:
|
||||
- title: Restart Plex
|
||||
icon: smile
|
||||
shell: docker restart plex
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#
|
||||
# Integration Test Config: General
|
||||
#
|
||||
#
|
||||
|
||||
showFooter: false
|
||||
checkForUpdates: false
|
||||
checkForUpdates: false
|
||||
|
||||
# Actions (buttons) to show up on the WebUI:
|
||||
actions:
|
||||
actions:
|
||||
- title: Ping example.com
|
||||
shell: ping example.com -c 1
|
||||
icon: ping
|
||||
@@ -1,14 +1,14 @@
|
||||
#
|
||||
# Integration Test Config: General
|
||||
#
|
||||
#
|
||||
|
||||
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||
|
||||
showNavigation: false
|
||||
checkForUpdates: false
|
||||
checkForUpdates: false
|
||||
|
||||
# Actions (buttons) to show up on the WebUI:
|
||||
actions:
|
||||
actions:
|
||||
- title: Ping example.com
|
||||
shell: ping example.com -c 1
|
||||
icon: ping
|
||||
25
integration-tests/configs/multipleDropdowns/config.yaml
Normal file
25
integration-tests/configs/multipleDropdowns/config.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||
|
||||
logLevel: "DEBUG"
|
||||
checkForUpdates: false
|
||||
|
||||
actions:
|
||||
- title: Ping Google.com
|
||||
shell: ping google.com -c 1
|
||||
icon: ping
|
||||
|
||||
- title: Test multiple dropdowns
|
||||
shell: echo {{ salutation }} {{ person }}
|
||||
icon: ping
|
||||
arguments:
|
||||
- name: salutation
|
||||
choices:
|
||||
- value: Hello
|
||||
- value: Goodbye
|
||||
|
||||
- name: person
|
||||
choices:
|
||||
- value: Alice
|
||||
- value: Bob
|
||||
- value: Dave
|
||||
28
integration-tests/configs/onlyDashboards/config.yaml
Normal file
28
integration-tests/configs/onlyDashboards/config.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
#
|
||||
# Integration Test Config: General
|
||||
#
|
||||
|
||||
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||
|
||||
logLevel: "DEBUG"
|
||||
checkForUpdates: false
|
||||
|
||||
actions:
|
||||
- title: action1
|
||||
shell: date
|
||||
icon: clock
|
||||
|
||||
- title: action2
|
||||
shell: date
|
||||
icon: clock
|
||||
|
||||
- title: action3
|
||||
shell: date
|
||||
icon: clock
|
||||
|
||||
dashboards:
|
||||
- title: My Dashboard
|
||||
contents:
|
||||
- title: action1
|
||||
- title: action2
|
||||
- title: action3
|
||||
15
integration-tests/configs/pageTitle/config.yaml
Normal file
15
integration-tests/configs/pageTitle/config.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
#
|
||||
# Integration Test Config: General
|
||||
#
|
||||
|
||||
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||
|
||||
logLevel: "DEBUG"
|
||||
checkForUpdates: false
|
||||
|
||||
pageTitle: "My Custom App"
|
||||
|
||||
actions:
|
||||
- title: sleep 2 seconds
|
||||
shell: sleep 2
|
||||
icon: "🥱"
|
||||
16
integration-tests/configs/prometheus/config.yaml
Normal file
16
integration-tests/configs/prometheus/config.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
#
|
||||
# Integration Test Config: General
|
||||
#
|
||||
|
||||
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||
|
||||
logLevel: "DEBUG"
|
||||
checkForUpdates: false
|
||||
|
||||
prometheus:
|
||||
enabled: true
|
||||
defaultGoMetrics: false
|
||||
|
||||
actions:
|
||||
- title: Hello OliveTin
|
||||
shell: echo "Hello OliveTin"
|
||||
14
integration-tests/configs/sleep/config.yaml
Normal file
14
integration-tests/configs/sleep/config.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
# Integration Test Config: Sleep
|
||||
#
|
||||
|
||||
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||
|
||||
logLevel: "DEBUG"
|
||||
checkForUpdates: false
|
||||
|
||||
actions:
|
||||
- title: Sleep
|
||||
shell: sleep 10
|
||||
popupOnStart: execution-dialog
|
||||
timeout: 9
|
||||
3
integration-tests/configs/trustedHeader/config.yaml
Normal file
3
integration-tests/configs/trustedHeader/config.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
logLevel: DEBUG
|
||||
|
||||
authHttpHeaderUsername: "X-User"
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:1337",
|
||||
"screenshotsFolder": "results/screenshots/",
|
||||
"videosFolder": "results/videos/"
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
describe('Homepage rendering', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit("/")
|
||||
});
|
||||
|
||||
it("Footer contains promo", () => {
|
||||
cy.get('footer').contains("OliveTin")
|
||||
})
|
||||
|
||||
it('Default buttons are rendered', () => {
|
||||
cy.get("#root-group button").should('have.length', 6)
|
||||
})
|
||||
|
||||
it('Switcher navigation is visible', () => {
|
||||
cy.get('#section-switcher').then($el => {
|
||||
expect(Cypress.dom.isHidden($el)).to.be.false
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
describe('Hidden Footer', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit("/")
|
||||
cy.wait(500)
|
||||
});
|
||||
|
||||
it('Footer is hidden', () => {
|
||||
cy.get('footer').then($el => {
|
||||
expect(Cypress.dom.isHidden($el)).to.be.true
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
describe('Hidden Nav', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit("/")
|
||||
cy.wait(500)
|
||||
});
|
||||
|
||||
it("Footer contains promo", () => {
|
||||
cy.get('footer').contains("OliveTin")
|
||||
})
|
||||
|
||||
it('Switcher navigation is hidden', () => {
|
||||
cy.get('#section-switcher').then($el => {
|
||||
expect(Cypress.dom.isHidden($el)).to.be.true
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -o xtrace
|
||||
|
||||
echo "Running config $1"
|
||||
|
||||
cp -f ./configs/config.$1.yaml ./configs/config.yaml
|
||||
docker start olivetin
|
||||
NO_COLOR=1 ./node_modules/.bin/cypress run --headless -s cypress/integration/$1/* || true
|
||||
docker kill olivetin
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# args:
|
||||
# $1: The Vagrant VM to test against. If blank and only one VM is provisioned, it will use that.
|
||||
|
||||
IP=$(vagrant ssh-config $1 | grep HostName | awk '{print $2}')
|
||||
BASE_URL="http://$IP:1337/"
|
||||
|
||||
echo "IP: $IP, BaseURL: $BASE_URL"
|
||||
|
||||
# Only run the general test, as we cannot easily switch out configs in VMs yet.
|
||||
./node_modules/.bin/cypress run --headless -c baseUrl=$BASE_URL -s cypress/integration/general/*
|
||||
8
integration-tests/envVagrant.sh
Executable file
8
integration-tests/envVagrant.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
# Run this like `. envVagrant.sh f38` before `mocha`
|
||||
|
||||
# args:
|
||||
# $1: The Vagrant VM to test against. If blank and only one VM is provisioned, it will use that.
|
||||
|
||||
export IP=$(vagrant ssh-config $1 | grep HostName | awk '{print $2}')
|
||||
export PORT=1337
|
||||
57
integration-tests/lib/elements.js
Normal file
57
integration-tests/lib/elements.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { By } from 'selenium-webdriver'
|
||||
import fs from 'fs'
|
||||
import { expect } from 'chai'
|
||||
import { Condition } from 'selenium-webdriver'
|
||||
|
||||
export async function getActionButtons (webdriver) {
|
||||
return await webdriver.findElement(By.id('contentActions')).findElements(By.tagName('button'))
|
||||
}
|
||||
|
||||
export function takeScreenshot (webdriver) {
|
||||
return webdriver.takeScreenshot().then((img) => {
|
||||
fs.writeFileSync('out.png', img, 'base64')
|
||||
})
|
||||
}
|
||||
|
||||
export async function getRootAndWait() {
|
||||
await webdriver.get(runner.baseUrl())
|
||||
await webdriver.wait(new Condition('wait for initial-marshal-complete', async function() {
|
||||
const body = await webdriver.findElement(By.tagName('body'))
|
||||
const attr = await body.getAttribute('initial-marshal-complete')
|
||||
|
||||
if (attr == 'true') {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
export async function requireExecutionDialogStatus (webdriver, expected) {
|
||||
// It seems that webdriver will not give us text if domStatus is hidden (which it will be until complete)
|
||||
await webdriver.executeScript('window.executionDialog.domExecutionDetails.hidden = false')
|
||||
|
||||
await webdriver.wait(new Condition('wait for action to be running', async function () {
|
||||
const actual = await webdriver.executeScript('return window.executionDialog.domStatus.getText()')
|
||||
|
||||
if (actual === expected) {
|
||||
return true
|
||||
} else {
|
||||
console.log('Waiting for domStatus text to be: ', expected, ', it is currently: ', actual)
|
||||
console.log(await webdriver.executeScript('return window.executionDialog.res'))
|
||||
return false
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
export async function findExecutionDialog (webdriver) {
|
||||
return webdriver.findElement(By.id('execution-results-popup'))
|
||||
}
|
||||
|
||||
export async function getActionButton (webdriver, title) {
|
||||
const buttons = await webdriver.findElements(By.css('[title="' + title + '"]'))
|
||||
|
||||
expect(buttons).to.have.length(1)
|
||||
|
||||
return buttons[0]
|
||||
}
|
||||
18
integration-tests/mochaSetup.mjs
Normal file
18
integration-tests/mochaSetup.mjs
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Options } from 'selenium-webdriver/chrome.js'
|
||||
import { Builder, Browser } from 'selenium-webdriver'
|
||||
import getRunner from './runner.mjs'
|
||||
|
||||
export async function mochaGlobalSetup () {
|
||||
const options = new Options()
|
||||
options.addArguments('--headless')
|
||||
|
||||
global.webdriver = await new Builder().forBrowser(Browser.CHROME).setChromeOptions(options).build()
|
||||
|
||||
global.runner = getRunner()
|
||||
|
||||
console.log('Runner constructor: ' + global.runner.constructor.name)
|
||||
}
|
||||
|
||||
export async function mochaGlobalTeardown () {
|
||||
await global.webdriver.quit()
|
||||
}
|
||||
3671
integration-tests/package-lock.json
generated
3671
integration-tests/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,22 @@
|
||||
{
|
||||
"name": "olivetin-integration-tests",
|
||||
"version": "1.0.0",
|
||||
"repository": "https://github.com/OliveTin/OliveTin-integration-tests",
|
||||
"description": "The cypress WebUI tests",
|
||||
"repository": "https://github.com/OliveTin/OliveTin",
|
||||
"description": "The integration-tests for OliveTin's webui.",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"license": "AGPL-3.0-only",
|
||||
"devDependencies": {
|
||||
"chai": "^5.1.0",
|
||||
"eslint": "^8.51.0",
|
||||
"mocha": "^10.4.0",
|
||||
"selenium-webdriver": "^4.19.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"cypress": "^8.3.0"
|
||||
"wait-on": "^7.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
3
integration-tests/proxies/caddy/Caddyfile
Normal file
3
integration-tests/proxies/caddy/Caddyfile
Normal file
@@ -0,0 +1,3 @@
|
||||
http://olivetin.example.com {
|
||||
reverse_proxy * http://localhost:1337
|
||||
}
|
||||
15
integration-tests/proxies/haproxy/haproxy.conf
Normal file
15
integration-tests/proxies/haproxy/haproxy.conf
Normal file
@@ -0,0 +1,15 @@
|
||||
frontend cleartext_frontend
|
||||
bind 0.0.0.0:80
|
||||
|
||||
option httplog
|
||||
|
||||
use_backend be_olivetin_webs if { hdr(Host) -i olivetin.example.com && path_beg /websocket }
|
||||
use_backend be_olivetin_http if { hdr(Host) -i olivetin.example.com }
|
||||
|
||||
backend be_olivetin_http
|
||||
server olivetinServer 127.0.0.1:1337 check
|
||||
|
||||
backend be_olivetin_webs
|
||||
timeout tunnel 1h
|
||||
option http-server-close
|
||||
server olivetinServer 127.0.0.1:1337
|
||||
9
integration-tests/proxies/httpd/OliveTin.conf
Normal file
9
integration-tests/proxies/httpd/OliveTin.conf
Normal file
@@ -0,0 +1,9 @@
|
||||
<VirtualHost *:80>
|
||||
ServerName olivetin.example.com
|
||||
ProxyPass / http://localhost:1337/
|
||||
ProxyPassReverse / http://localhost:1337/
|
||||
|
||||
RewriteEngine On
|
||||
RewriteCond %{REQUEST_URI} ^/websocket
|
||||
RewriteRule /(.) ws://localhost:1337/websocket [P,L]
|
||||
</VirtualHost>
|
||||
22
integration-tests/proxies/nginx/conf.d/OliveTin.conf
Normal file
22
integration-tests/proxies/nginx/conf.d/OliveTin.conf
Normal file
@@ -0,0 +1,22 @@
|
||||
server {
|
||||
listen 8443 ssl;
|
||||
|
||||
ssl_certificate "/etc/nginx/conf.d/server.crt";
|
||||
ssl_certificate_key "/etc/nginx/conf.d/server.key";
|
||||
|
||||
access_log /var/log/nginx/ot.access.log main;
|
||||
error_log /var/log/nginx/ot.error.log notice;
|
||||
|
||||
server_name olivetin.example.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://host.containers.internal:1337/;
|
||||
proxy_redirect http://host.containers.internal:1337/ http://host.containers.internal/OliveTin/;
|
||||
}
|
||||
|
||||
location /websocket {
|
||||
proxy_set_header Upgrade "websocket";
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_pass http://host.containers.internal:1337/websocket;
|
||||
}
|
||||
}
|
||||
1028
integration-tests/proxies/nginx/mime.types
Normal file
1028
integration-tests/proxies/nginx/mime.types
Normal file
File diff suppressed because it is too large
Load Diff
82
integration-tests/proxies/nginx/nginx.conf
Normal file
82
integration-tests/proxies/nginx/nginx.conf
Normal file
@@ -0,0 +1,82 @@
|
||||
# For more information on configuration, see:
|
||||
# * Official English Documentation: http://nginx.org/en/docs/
|
||||
# * Official Russian Documentation: http://nginx.org/ru/docs/
|
||||
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log notice;
|
||||
pid /run/nginx.pid;
|
||||
|
||||
# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
|
||||
include /usr/share/nginx/modules/*.conf;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 4096;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Load modular configuration files from the /etc/nginx/conf.d directory.
|
||||
# See http://nginx.org/en/docs/ngx_core_module.html#include
|
||||
# for more information.
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
# Load configuration files for the default server block.
|
||||
include /etc/nginx/default.d/*.conf;
|
||||
|
||||
error_page 404 /404.html;
|
||||
location = /404.html {
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
}
|
||||
}
|
||||
|
||||
# Settings for a TLS enabled server.
|
||||
#
|
||||
# server {
|
||||
# listen 8443 ssl http2;
|
||||
# listen [::]:8443 ssl http2;
|
||||
# server_name _;
|
||||
# root /usr/share/nginx/html;
|
||||
#
|
||||
# ssl_certificate "/etc/pki/nginx/server.crt";
|
||||
# ssl_certificate_key "/etc/pki/nginx/private/server.key";
|
||||
# ssl_session_cache shared:SSL:1m;
|
||||
# ssl_session_timeout 10m;
|
||||
# ssl_ciphers PROFILE=SYSTEM;
|
||||
# ssl_prefer_server_ciphers on;
|
||||
#
|
||||
# # Load configuration files for the default server block.
|
||||
# include /etc/nginx/default.d/*.conf;
|
||||
#
|
||||
# error_page 404 /404.html;
|
||||
# location = /404.html {
|
||||
# }
|
||||
#
|
||||
# error_page 500 502 503 504 /50x.html;
|
||||
# location = /50x.html {
|
||||
# }
|
||||
# }
|
||||
|
||||
}
|
||||
137
integration-tests/runner.mjs
Normal file
137
integration-tests/runner.mjs
Normal file
@@ -0,0 +1,137 @@
|
||||
import * as process from 'node:process'
|
||||
import waitOn from 'wait-on'
|
||||
import { spawn } from 'node:child_process'
|
||||
|
||||
export default function getRunner () {
|
||||
const type = process.env.OLIVETIN_TEST_RUNNER
|
||||
|
||||
console.log('OLIVETIN_TEST_RUNNER env value is: ', type)
|
||||
|
||||
switch (type) {
|
||||
case 'local':
|
||||
return new OliveTinTestRunnerStartLocalProcess()
|
||||
case 'vm':
|
||||
return new OliveTinTestRunnerVm()
|
||||
case 'container':
|
||||
return new OliveTinTestRunnerEnv()
|
||||
default:
|
||||
return new OliveTinTestRunnerStartLocalProcess()
|
||||
}
|
||||
}
|
||||
|
||||
class OliveTinTestRunner {
|
||||
BASE_URL = 'http://nohost:1337/';
|
||||
|
||||
baseUrl() {
|
||||
return this.BASE_URL
|
||||
}
|
||||
|
||||
metricsUrl() {
|
||||
return new URL('metrics', this.baseUrl());
|
||||
}
|
||||
}
|
||||
|
||||
class OliveTinTestRunnerStartLocalProcess extends OliveTinTestRunner {
|
||||
async start (cfg) {
|
||||
let stdout = ""
|
||||
let stderr = ""
|
||||
|
||||
this.ot = spawn('./../OliveTin', ['-configdir', 'configs/' + cfg + '/'])
|
||||
|
||||
let logStdout = false
|
||||
|
||||
if (process.env.CI === 'true') {
|
||||
logStdout = true;
|
||||
} else {
|
||||
logStdout = process.env.OLIVETIN_TEST_RUNNER_LOG_STDOUT === '1'
|
||||
}
|
||||
|
||||
this.ot.stdout.on('data', (data) => {
|
||||
stdout += data
|
||||
|
||||
if (logStdout) {
|
||||
console.log(`stdout: ${data}`)
|
||||
}
|
||||
})
|
||||
|
||||
this.ot.stderr.on('data', (data) => {
|
||||
stderr += data
|
||||
|
||||
if (logStdout) {
|
||||
console.log(`stderr: ${data}`)
|
||||
}
|
||||
})
|
||||
|
||||
this.ot.on('close', (code) => {
|
||||
if (code != null) {
|
||||
console.log(`OliveTin local process exited with code ${code}`)
|
||||
console.log(stdout)
|
||||
console.log(stderr)
|
||||
console.log(this.ot.exitCode)
|
||||
}
|
||||
})
|
||||
|
||||
if (this.ot.exitCode == null) {
|
||||
this.BASE_URL = 'http://localhost:1337/'
|
||||
|
||||
await waitOn({
|
||||
resources: [this.BASE_URL]
|
||||
})
|
||||
|
||||
console.log(" OliveTin local process started and webUI accessible")
|
||||
} else {
|
||||
console.log(" OliveTin local process start FAILED!")
|
||||
console.log(stdout)
|
||||
console.log(stderr)
|
||||
console.log(this.ot.exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
async stop () {
|
||||
if ((await this.ot.exitCode) != null) {
|
||||
console.log(" OliveTin local process tried stop(), but it already exited with code", this.ot.exitCode)
|
||||
} else {
|
||||
await this.ot.kill()
|
||||
console.log(" OliveTin local process killed")
|
||||
}
|
||||
|
||||
await new Promise((res) => setTimeout(res, 100))
|
||||
}
|
||||
}
|
||||
|
||||
class OliveTinTestRunnerEnv extends OliveTinTestRunner {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
const IP = process.env.IP
|
||||
const PORT = process.env.PORT
|
||||
|
||||
this.BASE_URL = 'http://' + IP + ':' + PORT + '/'
|
||||
|
||||
console.log('Runner ENV endpoint: ' + this.BASE_URL)
|
||||
}
|
||||
|
||||
async start () {
|
||||
await waitOn({
|
||||
resources: [this.BASE_URL]
|
||||
})
|
||||
}
|
||||
|
||||
async stop () {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class OliveTinTestRunnerVm extends OliveTinTestRunnerEnv {
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
async start (cfg) {
|
||||
console.log("vagrant changing config")
|
||||
spawn('vagrant', ['ssh', '-c', '"ln -sf /etc/OliveTin/ /opt/OliveTin-configs/' + cfg + '/config.yaml"'])
|
||||
spawn('vagrant', ['ssh', '-c', '"systemctl restart OliveTin"'])
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
30
integration-tests/test/entities.js
Normal file
30
integration-tests/test/entities.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { describe, it, before, after } from 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import { By, until } from 'selenium-webdriver'
|
||||
import { getRootAndWait, takeScreenshot } from '../lib/elements.js'
|
||||
|
||||
describe('config: entities', function () {
|
||||
before(async function () {
|
||||
await runner.start('entities')
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
it('Entity buttons are rendered', async function() {
|
||||
await getRootAndWait()
|
||||
|
||||
const buttons = await webdriver.findElement(By.id('root-group')).findElements(By.tagName('button'))
|
||||
expect(buttons).to.not.be.null
|
||||
expect(buttons).to.have.length(3)
|
||||
|
||||
expect(await buttons[0].getAttribute('title')).to.be.equal('Ping server1')
|
||||
expect(await buttons[1].getAttribute('title')).to.be.equal('Ping server2')
|
||||
expect(await buttons[2].getAttribute('title')).to.be.equal('Ping server3')
|
||||
|
||||
const dialogErr = await webdriver.findElement(By.id('big-error'))
|
||||
expect(dialogErr).to.not.be.null
|
||||
expect(await dialogErr.isDisplayed()).to.be.false
|
||||
})
|
||||
})
|
||||
95
integration-tests/test/general.mjs
Normal file
95
integration-tests/test/general.mjs
Normal file
@@ -0,0 +1,95 @@
|
||||
import { describe, it, before, after } from 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import { By, until, Condition } from 'selenium-webdriver'
|
||||
//import * as waitOn from 'wait-on'
|
||||
import { getRootAndWait, getActionButtons } from '../lib/elements.js'
|
||||
|
||||
describe('config: general', function () {
|
||||
before(async function () {
|
||||
await runner.start('general')
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
it('Page title', async function () {
|
||||
await webdriver.get(runner.baseUrl())
|
||||
|
||||
const title = await webdriver.getTitle()
|
||||
expect(title).to.be.equal("OliveTin")
|
||||
})
|
||||
|
||||
it('Page title2', async function () {
|
||||
/*
|
||||
await webdriver.get(runner.baseUrl())
|
||||
|
||||
const title = await webdriver.getTitle()
|
||||
expect(title).to.be.equal("OliveTin")
|
||||
*/
|
||||
})
|
||||
|
||||
|
||||
it('Footer contains promo', async function () {
|
||||
const ftr = await webdriver.findElement(By.tagName('footer')).getText()
|
||||
|
||||
expect(ftr).to.contain('Documentation')
|
||||
})
|
||||
|
||||
it('Default buttons are rendered', async function() {
|
||||
await getRootAndWait()
|
||||
|
||||
const buttons = await getActionButtons(webdriver)
|
||||
|
||||
expect(buttons).to.have.length(8)
|
||||
})
|
||||
|
||||
it('Start dir action (popup)', async function () {
|
||||
await getRootAndWait()
|
||||
|
||||
const buttons = await webdriver.findElements(By.css('[title="dir-popup"]'))
|
||||
|
||||
expect(buttons).to.have.length(1)
|
||||
|
||||
const buttonCMD = buttons[0]
|
||||
|
||||
expect(buttonCMD).to.not.be.null
|
||||
|
||||
buttonCMD.click()
|
||||
|
||||
const dialog = await webdriver.findElement(By.id('execution-results-popup'))
|
||||
expect(await dialog.isDisplayed()).to.be.true
|
||||
|
||||
const title = await webdriver.findElement(By.id('execution-dialog-title'))
|
||||
expect(await webdriver.wait(until.elementTextIs(title, 'dir-popup'), 2000))
|
||||
|
||||
const dialogErr = await webdriver.findElement(By.id('big-error'))
|
||||
expect(dialogErr).to.not.be.null
|
||||
expect(await dialogErr.isDisplayed()).to.be.false
|
||||
})
|
||||
|
||||
it('Start cd action (passive)', async function () {
|
||||
await getRootAndWait()
|
||||
|
||||
const buttons = await webdriver.findElements(By.css('[title="cd-passive"]'))
|
||||
|
||||
expect(buttons).to.have.length(1)
|
||||
|
||||
const buttonCMD = buttons[0]
|
||||
|
||||
expect(buttonCMD).to.not.be.null
|
||||
|
||||
buttonCMD.click()
|
||||
|
||||
const dialog = await webdriver.findElement(By.id('execution-results-popup'))
|
||||
expect(await dialog.isDisplayed()).to.be.false
|
||||
|
||||
const title = await webdriver.findElement(By.id('execution-dialog-title'))
|
||||
expect(await title.getAttribute('innerText')).to.be.equal('?')
|
||||
|
||||
const dialogErr = await webdriver.findElement(By.id('big-error'))
|
||||
expect(dialogErr).to.not.be.null
|
||||
expect(await dialogErr.isDisplayed()).to.be.false
|
||||
})
|
||||
|
||||
})
|
||||
22
integration-tests/test/hiddenFooter.mjs
Normal file
22
integration-tests/test/hiddenFooter.mjs
Normal file
@@ -0,0 +1,22 @@
|
||||
import { describe, it, before, after } from 'mocha'
|
||||
import { expect } from 'chai'
|
||||
|
||||
import { By } from 'selenium-webdriver'
|
||||
|
||||
describe('config: hiddenFooter', function () {
|
||||
before(async function () {
|
||||
await runner.start('hiddenFooter')
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
it('Check that footer is hidden', async () => {
|
||||
await webdriver.get(runner.baseUrl())
|
||||
|
||||
const footer = await webdriver.findElement(By.tagName('footer'))
|
||||
|
||||
expect(await footer.isDisplayed()).to.be.false
|
||||
})
|
||||
})
|
||||
20
integration-tests/test/hiddenNav.mjs
Normal file
20
integration-tests/test/hiddenNav.mjs
Normal file
@@ -0,0 +1,20 @@
|
||||
import { expect } from 'chai'
|
||||
import { By } from 'selenium-webdriver'
|
||||
|
||||
describe('config: hiddenNav', function () {
|
||||
before(async function () {
|
||||
await runner.start('hiddenNav')
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
it('nav is hidden', async () => {
|
||||
await webdriver.get(runner.baseUrl())
|
||||
|
||||
const toggler = await webdriver.findElement(By.tagName('header'))
|
||||
|
||||
expect(await toggler.isDisplayed()).to.be.false
|
||||
})
|
||||
})
|
||||
44
integration-tests/test/multipleDropdowns.js
Normal file
44
integration-tests/test/multipleDropdowns.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { describe, it, before, after } from 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import { By, until } from 'selenium-webdriver'
|
||||
import { getActionButtons, getRootAndWait } from '../lib/elements.js'
|
||||
|
||||
describe('config: multipleDropdowns', function () {
|
||||
before(async function () {
|
||||
await runner.start('multipleDropdowns')
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
it('Multiple dropdowns are possible', async function() {
|
||||
await getRootAndWait()
|
||||
|
||||
const buttons = await getActionButtons(webdriver)
|
||||
|
||||
let button = null
|
||||
for (const b of buttons) {
|
||||
const title = await b.getAttribute('title')
|
||||
|
||||
if (title === 'Test multiple dropdowns') {
|
||||
button = b
|
||||
}
|
||||
}
|
||||
|
||||
expect(buttons).to.have.length(2)
|
||||
expect(button).to.not.be.null
|
||||
|
||||
await button.click()
|
||||
|
||||
const dialog = await webdriver.findElement(By.id('argument-popup'))
|
||||
|
||||
await webdriver.wait(until.elementIsVisible(dialog), 3500)
|
||||
|
||||
const selects = await dialog.findElements(By.tagName('select'))
|
||||
|
||||
expect(selects).to.have.length(2)
|
||||
expect(await selects[0].findElements(By.tagName('option'))).to.have.length(2)
|
||||
expect(await selects[1].findElements(By.tagName('option'))).to.have.length(3)
|
||||
})
|
||||
})
|
||||
33
integration-tests/test/prometheus.mjs
Normal file
33
integration-tests/test/prometheus.mjs
Normal file
@@ -0,0 +1,33 @@
|
||||
import { describe, it, before, after } from 'mocha'
|
||||
import { expect } from 'chai'
|
||||
|
||||
import { By } from 'selenium-webdriver'
|
||||
|
||||
let metrics = [
|
||||
{'name': 'olivetin_actions_requested_count', 'type': 'counter', 'desc': 'The actions requested count'},
|
||||
{'name': 'olivetin_config_action_count', 'type': 'gauge', 'desc': 'The number of actions in the config file'},
|
||||
{'name': 'olivetin_config_reloaded_count', 'type': 'counter', 'desc': 'The number of times the config has been reloaded'},
|
||||
{'name': 'olivetin_sv_count', 'type': 'gauge', 'desc': 'The number entries in the sv map'},
|
||||
]
|
||||
|
||||
describe('config: prometheus', function () {
|
||||
before(async function () {
|
||||
await runner.start('prometheus')
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
it('Metrics are available with correct types', async () => {
|
||||
webdriver.get(runner.metricsUrl())
|
||||
const prometheusOutput = await webdriver.findElement(By.tagName('pre')).getText()
|
||||
|
||||
expect(prometheusOutput).to.not.be.null
|
||||
metrics.forEach(({name, type, desc}) => {
|
||||
const metaLines = `# HELP ${name} ${desc}\n`
|
||||
+ `# TYPE ${name} ${type}\n`
|
||||
expect(prometheusOutput).to.match(new RegExp(metaLines))
|
||||
})
|
||||
})
|
||||
})
|
||||
48
integration-tests/test/sleep.js
Normal file
48
integration-tests/test/sleep.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import * as process from 'node:process'
|
||||
import { describe, it, before, after } from 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import { By, Condition } from 'selenium-webdriver'
|
||||
import {
|
||||
takeScreenshot,
|
||||
findExecutionDialog,
|
||||
requireExecutionDialogStatus,
|
||||
getRootAndWait,
|
||||
getActionButton
|
||||
} from '../lib/elements.js'
|
||||
|
||||
describe('config: sleep', function () {
|
||||
before(async function () {
|
||||
await runner.start('sleep')
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
it('Sleep action kill', async function() {
|
||||
await getRootAndWait()
|
||||
|
||||
const btnSleep = await getActionButton(webdriver, "Sleep")
|
||||
|
||||
const dialog = await findExecutionDialog(webdriver)
|
||||
|
||||
expect(await dialog.isDisplayed()).to.be.false
|
||||
|
||||
await btnSleep.click()
|
||||
|
||||
expect(await dialog.isDisplayed()).to.be.true
|
||||
|
||||
await requireExecutionDialogStatus(webdriver, "unknown")
|
||||
|
||||
const killButton = await webdriver.findElement(By.id('execution-dialog-kill-action'))
|
||||
expect(killButton).to.not.be.undefined
|
||||
|
||||
await killButton.click()
|
||||
|
||||
console.log("env CI:", process.env.CI)
|
||||
|
||||
if (process.env.CI !== 'true') {
|
||||
await requireExecutionDialogStatus(webdriver, "Non-Zero Exit")
|
||||
}
|
||||
})
|
||||
})
|
||||
32
integration-tests/test/trustedHeader.js
Normal file
32
integration-tests/test/trustedHeader.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('config: trustedHeader', function () {
|
||||
before(async function () {
|
||||
await runner.start('trustedHeader')
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
it('req with X-User', async () => {
|
||||
const req = await fetch(runner.baseUrl() + '/api/WhoAmI', {
|
||||
headers: {
|
||||
"X-User": "fred",
|
||||
}
|
||||
})
|
||||
|
||||
if (!req.ok) {
|
||||
console.log(req)
|
||||
}
|
||||
|
||||
expect(req.ok, 'WhoAmI Request is ' + req.status).to.be.true
|
||||
|
||||
const json = await req.json()
|
||||
|
||||
expect(json).to.not.be.null
|
||||
expect(json).to.have.own.property('authenticatedUser')
|
||||
|
||||
expect(json['authenticatedUser']).to.be.equal('fred')
|
||||
})
|
||||
})
|
||||
@@ -9,6 +9,18 @@ import (
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
type PermissionBits int
|
||||
|
||||
const (
|
||||
View PermissionBits = 1 << iota
|
||||
Exec
|
||||
Logs
|
||||
)
|
||||
|
||||
func (p PermissionBits) Has(permission PermissionBits) bool {
|
||||
return p&permission != 0
|
||||
}
|
||||
|
||||
// User respresents a person.
|
||||
type AuthenticatedUser struct {
|
||||
Username string
|
||||
@@ -17,51 +29,101 @@ type AuthenticatedUser struct {
|
||||
acls []string
|
||||
}
|
||||
|
||||
// IsAllowedExec checks if a AuthenticatedUser is allowed to execute an Action
|
||||
func IsAllowedExec(cfg *config.Config, user *AuthenticatedUser, action *config.Action) bool {
|
||||
for _, acl := range getRelevantAcls(cfg, action.Acls, user) {
|
||||
if acl.Permissions.Exec {
|
||||
log.WithFields(log.Fields{
|
||||
"User": user.Username,
|
||||
"Action": action.Title,
|
||||
"ACL": acl.Name,
|
||||
}).Debug("isAllowedExec - Matched ACL")
|
||||
func logAclNotMatched(cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action, acl *config.AccessControlList) {
|
||||
if cfg.LogDebugOptions.AclNotMatched {
|
||||
log.WithFields(log.Fields{
|
||||
"User": user.Username,
|
||||
"Action": action.Title,
|
||||
}).Debugf("%v - No ACLs Matched", aclFunction)
|
||||
}
|
||||
}
|
||||
|
||||
func logAclMatched(cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action, acl *config.AccessControlList) {
|
||||
if cfg.LogDebugOptions.AclMatched {
|
||||
log.WithFields(log.Fields{
|
||||
"User": user.Username,
|
||||
"Action": action.Title,
|
||||
"ACL": acl.Name,
|
||||
}).Debugf("%v - Matched ACL", aclFunction)
|
||||
}
|
||||
}
|
||||
|
||||
func logAclNoneMatched(cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action, defaultPermission bool) {
|
||||
if cfg.LogDebugOptions.AclNoneMatched {
|
||||
log.WithFields(log.Fields{
|
||||
"User": user.Username,
|
||||
"Action": action.Title,
|
||||
"Default": defaultPermission,
|
||||
}).Debugf("%v - No ACLs Matched, returning default permission", aclFunction)
|
||||
}
|
||||
}
|
||||
|
||||
func permissionsConfigToBits(permissions config.PermissionsList) PermissionBits {
|
||||
var ret PermissionBits
|
||||
|
||||
if permissions.View {
|
||||
ret |= View
|
||||
}
|
||||
|
||||
if permissions.Exec {
|
||||
ret |= Exec
|
||||
}
|
||||
|
||||
if permissions.Logs {
|
||||
ret |= Logs
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func aclCheck(requiredPermission PermissionBits, defaultValue bool, cfg *config.Config, aclFunction string, user *AuthenticatedUser, action *config.Action) bool {
|
||||
relevantAcls := getRelevantAcls(cfg, action.Acls, user)
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"actionTitle": action.Title,
|
||||
"username": user.Username,
|
||||
"usergroup": user.Usergroup,
|
||||
"relevantAcls": len(relevantAcls),
|
||||
"requiredPermission": requiredPermission,
|
||||
}).Debugf("ACL check - %v", aclFunction)
|
||||
|
||||
for _, acl := range relevantAcls {
|
||||
permissionBits := permissionsConfigToBits(acl.Permissions)
|
||||
|
||||
if permissionBits.Has(requiredPermission) {
|
||||
logAclMatched(cfg, aclFunction, user, action, acl)
|
||||
|
||||
return true
|
||||
} else {
|
||||
logAclNotMatched(cfg, aclFunction, user, action, acl)
|
||||
}
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"User": user.Username,
|
||||
"Action": action.Title,
|
||||
}).Debug("isAllowedExec - No ACLs matched")
|
||||
logAclNoneMatched(cfg, aclFunction, user, action, cfg.DefaultPermissions.Logs)
|
||||
|
||||
return cfg.DefaultPermissions.Exec
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// IsAllowedLogs checks if a AuthenticatedUser is allowed to view an action's logs
|
||||
func IsAllowedLogs(cfg *config.Config, user *AuthenticatedUser, action *config.Action) bool {
|
||||
return aclCheck(Logs, cfg.DefaultPermissions.Logs, cfg, "isAllowedLogs", user, action)
|
||||
}
|
||||
|
||||
// IsAllowedExec checks if a AuthenticatedUser is allowed to execute an Action
|
||||
func IsAllowedExec(cfg *config.Config, user *AuthenticatedUser, action *config.Action) bool {
|
||||
return aclCheck(Exec, cfg.DefaultPermissions.Exec, cfg, "isAllowedExec", user, action)
|
||||
}
|
||||
|
||||
// IsAllowedView checks if a User is allowed to view an Action
|
||||
func IsAllowedView(cfg *config.Config, user *AuthenticatedUser, action *config.Action) bool {
|
||||
for _, acl := range getRelevantAcls(cfg, action.Acls, user) {
|
||||
if acl.Permissions.View {
|
||||
log.WithFields(log.Fields{
|
||||
"User": user.Username,
|
||||
"Action": action.Title,
|
||||
"ACL": acl.Name,
|
||||
}).Debug("isAllowedView - Matched ACL")
|
||||
|
||||
return true
|
||||
}
|
||||
if action.Hidden {
|
||||
return false
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"User": user.Username,
|
||||
"Action": action.Title,
|
||||
}).Debug("isAllowedView - No ACLs matched")
|
||||
|
||||
return cfg.DefaultPermissions.View
|
||||
return aclCheck(View, cfg.DefaultPermissions.View, cfg, "isAllowedView", user, action)
|
||||
}
|
||||
|
||||
func getMetdataKeyOrEmpty(md metadata.MD, key string) string {
|
||||
func getMetadataKeyOrEmpty(md metadata.MD, key string) string {
|
||||
mdValues := md.Get(key)
|
||||
|
||||
if len(mdValues) > 0 {
|
||||
@@ -73,13 +135,21 @@ func getMetdataKeyOrEmpty(md metadata.MD, key string) string {
|
||||
|
||||
// UserFromContext tries to find a user from a grpc context
|
||||
func UserFromContext(ctx context.Context, cfg *config.Config) *AuthenticatedUser {
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
|
||||
ret := &AuthenticatedUser{}
|
||||
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
|
||||
if ok {
|
||||
ret.Username = getMetdataKeyOrEmpty(md, "username")
|
||||
ret.Usergroup = getMetdataKeyOrEmpty(md, "usergroup")
|
||||
ret.Username = getMetadataKeyOrEmpty(md, "username")
|
||||
ret.Usergroup = getMetadataKeyOrEmpty(md, "usergroup")
|
||||
}
|
||||
|
||||
if ret.Username == "" {
|
||||
ret.Username = "guest"
|
||||
}
|
||||
|
||||
if ret.Usergroup == "" {
|
||||
ret.Usergroup = "guest"
|
||||
}
|
||||
|
||||
buildUserAcls(cfg, ret)
|
||||
@@ -87,7 +157,18 @@ func UserFromContext(ctx context.Context, cfg *config.Config) *AuthenticatedUser
|
||||
log.WithFields(log.Fields{
|
||||
"username": ret.Username,
|
||||
"usergroup": ret.Usergroup,
|
||||
}).Infof("UserFromContext")
|
||||
}).Debugf("UserFromContext")
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func UserFromSystem(cfg *config.Config, username string) *AuthenticatedUser {
|
||||
ret := &AuthenticatedUser{
|
||||
Username: username,
|
||||
Usergroup: "system",
|
||||
}
|
||||
|
||||
buildUserAcls(cfg, ret)
|
||||
|
||||
return ret
|
||||
}
|
||||
@@ -107,8 +188,10 @@ func buildUserAcls(cfg *config.Config, user *AuthenticatedUser) {
|
||||
}
|
||||
}
|
||||
|
||||
func isACLRelevant(cfg *config.Config, actionAcls []string, acl config.AccessControlList, user *AuthenticatedUser) bool {
|
||||
func isACLRelevantToAction(cfg *config.Config, actionAcls []string, acl *config.AccessControlList, user *AuthenticatedUser) bool {
|
||||
if !slices.Contains(user.acls, acl.Name) {
|
||||
// If the user does not have this ACL, then it is not relevant
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -127,8 +210,8 @@ func getRelevantAcls(cfg *config.Config, actionAcls []string, user *Authenticate
|
||||
var ret []*config.AccessControlList
|
||||
|
||||
for _, acl := range cfg.AccessControlLists {
|
||||
if isACLRelevant(cfg, actionAcls, acl, user) {
|
||||
ret = append(ret, &acl)
|
||||
if isACLRelevantToAction(cfg, actionAcls, acl, user) {
|
||||
ret = append(ret, acl)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,32 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Action represents the core functionality of OliveTin - commands that show up
|
||||
// as buttons in the UI.
|
||||
type Action struct {
|
||||
ID string
|
||||
Title string
|
||||
Icon string
|
||||
Shell string
|
||||
CSS map[string]string `mapstructure:"omitempty"`
|
||||
Timeout int
|
||||
Acls []string
|
||||
ExecOnStartup bool
|
||||
ExecOnCron []string
|
||||
Arguments []ActionArgument
|
||||
ID string
|
||||
Title string
|
||||
Icon string
|
||||
Shell string
|
||||
ShellAfterCompleted string
|
||||
Timeout int
|
||||
Acls []string
|
||||
Entity string
|
||||
Hidden bool
|
||||
ExecOnStartup bool
|
||||
ExecOnCron []string
|
||||
ExecOnFileCreatedInDir []string
|
||||
ExecOnFileChangedInDir []string
|
||||
ExecOnCalendarFile string
|
||||
Trigger string
|
||||
MaxConcurrent int
|
||||
MaxRate []RateSpec
|
||||
Arguments []ActionArgument
|
||||
PopupOnStart string
|
||||
SaveLogs SaveLogsConfig
|
||||
}
|
||||
|
||||
// ActionArgument objects appear on Actions.
|
||||
@@ -23,6 +37,9 @@ type ActionArgument struct {
|
||||
Type string
|
||||
Default string
|
||||
Choices []ActionArgumentChoice
|
||||
Entity string
|
||||
RejectNull bool
|
||||
Suggestions map[string]string
|
||||
}
|
||||
|
||||
// ActionArgumentChoice represents a predefined choice for an argument.
|
||||
@@ -31,19 +48,25 @@ type ActionArgumentChoice struct {
|
||||
Title string
|
||||
}
|
||||
|
||||
// RateSpec allows you to set a max frequency for an action.
|
||||
type RateSpec struct {
|
||||
Limit int
|
||||
Duration string
|
||||
}
|
||||
|
||||
// Entity represents a "thing" that can have multiple actions associated with it.
|
||||
// for example, a media player with a start and stop action.
|
||||
type Entity struct {
|
||||
Title string
|
||||
Icon string
|
||||
Actions []Action `mapstructure:"actions"`
|
||||
CSS map[string]string
|
||||
type EntityFile struct {
|
||||
File string
|
||||
Name string
|
||||
Icon string
|
||||
}
|
||||
|
||||
// PermissionsList defines what users can do with an action.
|
||||
type PermissionsList struct {
|
||||
View bool
|
||||
Exec bool
|
||||
Logs bool
|
||||
}
|
||||
|
||||
// AccessControlList defines what permissions apply to a user or user group.
|
||||
@@ -55,54 +78,125 @@ type AccessControlList struct {
|
||||
Permissions PermissionsList
|
||||
}
|
||||
|
||||
type PrometheusConfig struct {
|
||||
Enabled bool
|
||||
DefaultGoMetrics bool
|
||||
}
|
||||
|
||||
// Config is the global config used through the whole app.
|
||||
type Config struct {
|
||||
UseSingleHTTPFrontend bool
|
||||
ThemeName string
|
||||
ThemeCacheDisabled bool
|
||||
ListenAddressSingleHTTPFrontend string
|
||||
ListenAddressWebUI string
|
||||
ListenAddressRestActions string
|
||||
ListenAddressGrpcActions string
|
||||
ListenAddressPrometheus string
|
||||
ExternalRestAddress string
|
||||
LogLevel string
|
||||
Actions []Action `mapstructure:"actions"`
|
||||
Entities []Entity `mapstructure:"entities"`
|
||||
LogDebugOptions LogDebugOptions
|
||||
Actions []*Action `mapstructure:"actions"`
|
||||
Entities []*EntityFile `mapstructure:"entities"`
|
||||
Dashboards []*DashboardComponent `mapstructure:"dashboards"`
|
||||
CheckForUpdates bool
|
||||
PageTitle string
|
||||
ShowFooter bool
|
||||
ShowNavigation bool
|
||||
ShowNewVersions bool
|
||||
EnableCustomJs bool
|
||||
AuthJwtCookieName string
|
||||
AuthJwtSecret string
|
||||
AuthJwtAud string
|
||||
AuthJwtDomain string
|
||||
AuthJwtCertsURL string
|
||||
AuthJwtHmacSecret string // mutually exclusive with pub key config fields
|
||||
AuthJwtClaimUsername string
|
||||
AuthJwtClaimUserGroup string
|
||||
AuthJwtPubKeyPath string // will read pub key from file on disk
|
||||
AuthHttpHeaderUsername string
|
||||
AuthHttpHeaderUserGroup string
|
||||
DefaultPermissions PermissionsList
|
||||
AccessControlLists []AccessControlList
|
||||
AccessControlLists []*AccessControlList
|
||||
WebUIDir string
|
||||
CronSupportForSeconds bool
|
||||
SectionNavigationStyle string
|
||||
DefaultPopupOnStart string
|
||||
InsecureAllowDumpVars bool
|
||||
InsecureAllowDumpSos bool
|
||||
InsecureAllowDumpActionMap bool
|
||||
InsecureAllowDumpJwtClaims bool
|
||||
Prometheus PrometheusConfig
|
||||
SaveLogs SaveLogsConfig
|
||||
DefaultIconForActions string
|
||||
DefaultIconForDirectories string
|
||||
DefaultIconForBack string
|
||||
|
||||
usedConfigDir string
|
||||
}
|
||||
|
||||
type SaveLogsConfig struct {
|
||||
ResultsDirectory string
|
||||
OutputDirectory string
|
||||
}
|
||||
|
||||
type LogDebugOptions struct {
|
||||
SingleFrontendRequests bool
|
||||
SingleFrontendRequestHeaders bool
|
||||
AclMatched bool
|
||||
AclNotMatched bool
|
||||
AclNoneMatched bool
|
||||
}
|
||||
|
||||
type DashboardComponent struct {
|
||||
Title string
|
||||
Type string
|
||||
Entity string
|
||||
Icon string
|
||||
CssClass string
|
||||
Contents []DashboardComponent
|
||||
}
|
||||
|
||||
func DefaultConfig() *Config {
|
||||
return DefaultConfigWithBasePort(1337)
|
||||
}
|
||||
|
||||
// DefaultConfig gets a new Config structure with sensible default values.
|
||||
func DefaultConfig() *Config {
|
||||
func DefaultConfigWithBasePort(basePort int) *Config {
|
||||
config := Config{}
|
||||
config.UseSingleHTTPFrontend = true
|
||||
config.PageTitle = "OliveTin"
|
||||
config.ShowFooter = true
|
||||
config.ShowNavigation = true
|
||||
config.ShowNewVersions = true
|
||||
config.ListenAddressSingleHTTPFrontend = "0.0.0.0:1337"
|
||||
config.ListenAddressRestActions = "localhost:1338"
|
||||
config.ListenAddressGrpcActions = "localhost:1339"
|
||||
config.ListenAddressWebUI = "localhost:1340"
|
||||
config.EnableCustomJs = false
|
||||
config.ExternalRestAddress = "."
|
||||
config.LogLevel = "INFO"
|
||||
config.CheckForUpdates = true
|
||||
config.CheckForUpdates = false
|
||||
config.DefaultPermissions.Exec = true
|
||||
config.DefaultPermissions.View = true
|
||||
config.DefaultPermissions.Logs = true
|
||||
config.AuthJwtClaimUsername = "name"
|
||||
config.AuthJwtClaimUserGroup = "group"
|
||||
config.WebUIDir = "./webui"
|
||||
config.CronSupportForSeconds = false
|
||||
config.SectionNavigationStyle = "sidebar"
|
||||
config.DefaultPopupOnStart = "nothing"
|
||||
config.InsecureAllowDumpVars = false
|
||||
config.InsecureAllowDumpSos = false
|
||||
config.InsecureAllowDumpActionMap = false
|
||||
config.InsecureAllowDumpJwtClaims = false
|
||||
config.Prometheus.Enabled = false
|
||||
config.Prometheus.DefaultGoMetrics = false
|
||||
config.DefaultIconForActions = "😀"
|
||||
config.DefaultIconForDirectories = "📁"
|
||||
config.DefaultIconForBack = "«"
|
||||
config.ThemeCacheDisabled = false
|
||||
|
||||
config.ListenAddressSingleHTTPFrontend = fmt.Sprintf("0.0.0.0:%d", basePort)
|
||||
config.ListenAddressRestActions = fmt.Sprintf("localhost:%d", basePort+1)
|
||||
config.ListenAddressGrpcActions = fmt.Sprintf("localhost:%d", basePort+2)
|
||||
config.ListenAddressWebUI = fmt.Sprintf("localhost:%d", basePort+3)
|
||||
config.ListenAddressPrometheus = fmt.Sprintf("localhost:%d", basePort+4)
|
||||
|
||||
return &config
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ package config
|
||||
func (cfg *Config) FindAction(actionTitle string) *Action {
|
||||
for _, action := range cfg.Actions {
|
||||
if action.Title == actionTitle {
|
||||
return &action
|
||||
return action
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,17 @@ func (cfg *Config) FindAction(actionTitle string) *Action {
|
||||
|
||||
// FindArg will return an arg if there is a match on Name
|
||||
func (action *Action) FindArg(name string) *ActionArgument {
|
||||
if name == "stdout" || name == "exitCode" {
|
||||
return &ActionArgument{
|
||||
Name: name,
|
||||
Type: "very_dangerous_raw_string",
|
||||
}
|
||||
}
|
||||
|
||||
return action.findArg(name)
|
||||
}
|
||||
|
||||
func (action *Action) findArg(name string) *ActionArgument {
|
||||
for _, arg := range action.Arguments {
|
||||
if arg.Name == name {
|
||||
return &arg
|
||||
@@ -25,9 +36,17 @@ func (action *Action) FindArg(name string) *ActionArgument {
|
||||
func (cfg *Config) FindAcl(aclTitle string) *AccessControlList {
|
||||
for _, acl := range cfg.AccessControlLists {
|
||||
if acl.Name == aclTitle {
|
||||
return &acl
|
||||
return acl
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Config) SetDir(dir string) {
|
||||
cfg.usedConfigDir = dir
|
||||
}
|
||||
|
||||
func (cfg *Config) GetDir() string {
|
||||
return cfg.usedConfigDir
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ import (
|
||||
func TestFindAction(t *testing.T) {
|
||||
c := DefaultConfig()
|
||||
|
||||
a1 := Action{}
|
||||
a1 := &Action{}
|
||||
a1.Title = "a1"
|
||||
c.Actions = append(c.Actions, a1)
|
||||
|
||||
a2 := Action{
|
||||
a2 := &Action{
|
||||
Title: "a2",
|
||||
Arguments: []ActionArgument{
|
||||
{
|
||||
@@ -35,7 +35,7 @@ func TestFindAction(t *testing.T) {
|
||||
func TestFindAcl(t *testing.T) {
|
||||
c := DefaultConfig()
|
||||
|
||||
acl1 := AccessControlList{
|
||||
acl1 := &AccessControlList{
|
||||
Name: "Testing ACL",
|
||||
}
|
||||
|
||||
@@ -44,3 +44,10 @@ func TestFindAcl(t *testing.T) {
|
||||
assert.NotNil(t, c.FindAcl("Testing ACL"), "Find a ACL that should exist")
|
||||
assert.Nil(t, c.FindAcl("Chocolate Cake"), "Find a ACL that does not exist")
|
||||
}
|
||||
|
||||
func TestSetDir(t *testing.T) {
|
||||
c := DefaultConfig()
|
||||
c.SetDir("test")
|
||||
|
||||
assert.Equal(t, "test", c.GetDir(), "SetDir")
|
||||
}
|
||||
|
||||
48
internal/config/config_reloader.go
Normal file
48
internal/config/config_reloader.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
metricConfigActionCount = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "olivetin_config_action_count",
|
||||
Help: "The number of actions in the config file",
|
||||
})
|
||||
|
||||
metricConfigReloadedCount = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "olivetin_config_reloaded_count",
|
||||
Help: "The number of times the config has been reloaded",
|
||||
})
|
||||
|
||||
listeners []func()
|
||||
)
|
||||
|
||||
func AddListener(l func()) {
|
||||
listeners = append(listeners, l)
|
||||
}
|
||||
|
||||
func Reload(cfg *Config) {
|
||||
if err := viper.UnmarshalExact(&cfg); err != nil {
|
||||
log.Errorf("Config unmarshal error %+v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
metricConfigReloadedCount.Inc()
|
||||
metricConfigActionCount.Set(float64(len(cfg.Actions)))
|
||||
|
||||
cfg.SetDir(filepath.Dir(viper.ConfigFileUsed()))
|
||||
cfg.Sanitize()
|
||||
|
||||
for _, l := range listeners {
|
||||
l()
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,28 @@
|
||||
package config
|
||||
|
||||
var emojis = map[string]string{
|
||||
"": "😀", // default icon
|
||||
"poop": "💩",
|
||||
"smile": "😀",
|
||||
"ping": "📡",
|
||||
"backup": "💾",
|
||||
"reboot": "⏻",
|
||||
"restart": "⏻",
|
||||
"poop": "💩",
|
||||
"smile": "😀",
|
||||
"ping": "📡",
|
||||
"backup": "💾",
|
||||
"reboot": "🔄",
|
||||
"restart": "🔄",
|
||||
"box": "📦",
|
||||
"ashtonished": "😲",
|
||||
"clock": "🕒",
|
||||
"disk": "💽",
|
||||
"logs": "🔍",
|
||||
"light": "💡",
|
||||
"robot": "🤖",
|
||||
"ssh": "🔐",
|
||||
"theme": "🎨",
|
||||
}
|
||||
|
||||
func lookupHTMLIcon(keyToLookup string) string {
|
||||
func lookupHTMLIcon(keyToLookup string, defaultIcon string) string {
|
||||
if keyToLookup == "" {
|
||||
return defaultIcon
|
||||
}
|
||||
|
||||
if emoji, found := emojis[keyToLookup]; found {
|
||||
return emoji
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ import (
|
||||
)
|
||||
|
||||
func TestGetEmojiByShortName(t *testing.T) {
|
||||
assert.Equal(t, "😀", lookupHTMLIcon("smile"), "Find an eomji by short name")
|
||||
assert.Equal(t, "😀", lookupHTMLIcon("smile", "empty"), "Find an eomji by short name")
|
||||
|
||||
assert.Equal(t, "notfound", lookupHTMLIcon("notfound"), "Find an eomji by undefined short name")
|
||||
assert.Equal(t, "empty", lookupHTMLIcon("", "empty"), "Find an eomji when the value is empty")
|
||||
|
||||
assert.Equal(t, "notfound", lookupHTMLIcon("notfound", "empty"), "Find an eomji by undefined short name")
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Sanitize will look for common configuration issues, and fix them. For example,
|
||||
@@ -12,7 +14,7 @@ func (cfg *Config) Sanitize() {
|
||||
// log.Infof("cfg %p", cfg)
|
||||
|
||||
for idx := range cfg.Actions {
|
||||
cfg.Actions[idx].sanitize()
|
||||
cfg.Actions[idx].sanitize(cfg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,18 +25,49 @@ func (cfg *Config) sanitizeLogLevel() {
|
||||
}
|
||||
}
|
||||
|
||||
func (action *Action) sanitize() {
|
||||
func (action *Action) sanitize(cfg *Config) {
|
||||
if action.Timeout < 3 {
|
||||
action.Timeout = 3
|
||||
}
|
||||
|
||||
action.Icon = lookupHTMLIcon(action.Icon)
|
||||
action.ID = getActionID(action)
|
||||
action.Icon = lookupHTMLIcon(action.Icon, cfg.DefaultIconForActions)
|
||||
action.PopupOnStart = sanitizePopupOnStart(action.PopupOnStart, cfg)
|
||||
|
||||
if action.MaxConcurrent < 1 {
|
||||
action.MaxConcurrent = 1
|
||||
}
|
||||
|
||||
for idx := range action.Arguments {
|
||||
action.Arguments[idx].sanitize()
|
||||
}
|
||||
}
|
||||
|
||||
func getActionID(action *Action) string {
|
||||
if action.ID == "" {
|
||||
return uuid.NewString()
|
||||
}
|
||||
|
||||
if strings.Contains(action.ID, "{{") {
|
||||
log.Fatalf("Action IDs cannot contain variables")
|
||||
}
|
||||
|
||||
return action.ID
|
||||
}
|
||||
|
||||
func sanitizePopupOnStart(raw string, cfg *Config) string {
|
||||
switch raw {
|
||||
case "execution-dialog":
|
||||
return raw
|
||||
case "execution-dialog-stdout-only":
|
||||
return raw
|
||||
case "execution-button":
|
||||
return raw
|
||||
default:
|
||||
return cfg.DefaultPopupOnStart
|
||||
}
|
||||
}
|
||||
|
||||
func (arg *ActionArgument) sanitize() {
|
||||
if arg.Title == "" {
|
||||
arg.Title = arg.Name
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
func TestSanitizeConfig(t *testing.T) {
|
||||
c := DefaultConfig()
|
||||
|
||||
a := Action{
|
||||
a := &Action{
|
||||
Title: "Mr Waffles",
|
||||
Arguments: []ActionArgument{
|
||||
{
|
||||
|
||||
162
internal/entityfiles/entityfiles.go
Normal file
162
internal/entityfiles/entityfiles.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package entityfiles
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
"github.com/OliveTin/OliveTin/internal/filehelper"
|
||||
sv "github.com/OliveTin/OliveTin/internal/stringvariables"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
EntityChangedSender chan bool
|
||||
listeners []func()
|
||||
)
|
||||
|
||||
func AddListener(l func()) {
|
||||
listeners = append(listeners, l)
|
||||
}
|
||||
|
||||
func SetupEntityFileWatchers(cfg *config.Config) {
|
||||
configDir := cfg.GetDir()
|
||||
|
||||
configDirVar := filepath.Join(configDir, "var") // for development purposes
|
||||
|
||||
if _, err := os.Stat(configDirVar); err == nil {
|
||||
configDir = configDirVar
|
||||
}
|
||||
|
||||
for entityIndex, _ := range cfg.Entities { // #337 - iterate by key, not by value
|
||||
ef := cfg.Entities[entityIndex]
|
||||
p := ef.File
|
||||
|
||||
if !filepath.IsAbs(p) {
|
||||
p = filepath.Join(configDir, p)
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"entityFile": p,
|
||||
}).Debugf("Adding config dir to entity file path")
|
||||
}
|
||||
|
||||
go filehelper.WatchFileWrite(p, func(filename string) {
|
||||
loadEntityFile(p, ef.Name)
|
||||
})
|
||||
|
||||
loadEntityFile(p, ef.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func loadEntityFile(filename string, entityname string) {
|
||||
if strings.HasSuffix(filename, ".json") {
|
||||
loadEntityFileJson(filename, entityname)
|
||||
} else {
|
||||
loadEntityFileYaml(filename, entityname)
|
||||
}
|
||||
}
|
||||
|
||||
func loadEntityFileJson(filename string, entityname string) {
|
||||
log.WithFields(log.Fields{
|
||||
"file": filename,
|
||||
"name": entityname,
|
||||
}).Infof("Loading entity file with JSON format")
|
||||
|
||||
jfile, err := os.ReadFile(filename)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("ReadIn: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
data := make([]map[string]interface{}, 0)
|
||||
|
||||
decoder := json.NewDecoder(bytes.NewReader(jfile))
|
||||
|
||||
for decoder.More() {
|
||||
d := make(map[string]interface{})
|
||||
|
||||
err := decoder.Decode(&d)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
data = append(data, d)
|
||||
}
|
||||
|
||||
updateSvFromFile(entityname, data)
|
||||
}
|
||||
|
||||
func loadEntityFileYaml(filename string, entityname string) {
|
||||
log.WithFields(log.Fields{
|
||||
"file": filename,
|
||||
"name": entityname,
|
||||
}).Infof("Loading entity file with YAML format")
|
||||
|
||||
yfile, err := os.ReadFile(filename)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("ReadIn: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
data := make([]map[string]interface{}, 1)
|
||||
|
||||
err = yaml.Unmarshal(yfile, &data)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Unmarshal: %v", err)
|
||||
}
|
||||
|
||||
updateSvFromFile(entityname, data)
|
||||
}
|
||||
|
||||
func updateSvFromFile(entityname string, data []map[string]interface{}) {
|
||||
log.Debugf("updateSvFromFile: %+v", data)
|
||||
|
||||
count := len(data)
|
||||
|
||||
sv.RemoveKeysThatStartWith("entities." + entityname)
|
||||
|
||||
sv.SetEntityCount(entityname, count)
|
||||
|
||||
for i, mapp := range data {
|
||||
prefix := "entities." + entityname + "." + fmt.Sprintf("%v", i)
|
||||
|
||||
serializeValueToSv(prefix, mapp)
|
||||
}
|
||||
|
||||
for _, l := range listeners {
|
||||
l()
|
||||
}
|
||||
}
|
||||
|
||||
func serializeValueToSv(prefix string, value interface{}) {
|
||||
if m, ok := value.(map[string]interface{}); ok { // if value is a map we need to flatten it
|
||||
serializeMapToSv(prefix, m)
|
||||
} else if s, ok := value.([]interface{}); ok { // if value is a slice we need to flatten it
|
||||
serializeSliceToSv(prefix, s)
|
||||
} else {
|
||||
sv.Set(prefix, fmt.Sprintf("%v", value))
|
||||
}
|
||||
}
|
||||
|
||||
func serializeMapToSv(prefix string, m map[string]interface{}) {
|
||||
for k, v := range m {
|
||||
serializeValueToSv(prefix+"."+k, v)
|
||||
}
|
||||
}
|
||||
|
||||
func serializeSliceToSv(prefix string, s []interface{}) {
|
||||
sv.Set(prefix+".count", fmt.Sprintf("%v", len(s)))
|
||||
|
||||
for i, v := range s {
|
||||
serializeValueToSv(prefix+"."+fmt.Sprintf("%v", i), v)
|
||||
}
|
||||
}
|
||||
@@ -2,28 +2,32 @@ package executor
|
||||
|
||||
import (
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
sv "github.com/OliveTin/OliveTin/internal/stringvariables"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"errors"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
typecheckRegex = map[string]string{
|
||||
"very_dangerous_raw_string": "",
|
||||
"int": "^[\\d]+$",
|
||||
"unicode_identifier": "^[\\w\\/\\\\.\\_ \\d]+$",
|
||||
"ascii": "^[a-zA-Z0-9]+$",
|
||||
"ascii_identifier": "^[a-zA-Z0-9\\-\\.\\_]+$",
|
||||
"ascii_sentence": "^[a-zA-Z0-9 \\,\\.]+$",
|
||||
}
|
||||
)
|
||||
|
||||
func parseActionArguments(rawShellCommand string, values map[string]string, action *config.Action) (string, error) {
|
||||
func parseActionArguments(rawShellCommand string, values map[string]string, action *config.Action, actionTitle string, entityPrefix string) (string, error) {
|
||||
log.WithFields(log.Fields{
|
||||
"cmd": rawShellCommand,
|
||||
}).Infof("Before Parse Args")
|
||||
"actionTitle": actionTitle,
|
||||
"cmd": rawShellCommand,
|
||||
}).Infof("Action parse args - Before")
|
||||
|
||||
r := regexp.MustCompile("{{ *?([a-zA-Z0-9_]+?) *?}}")
|
||||
matches := r.FindAllStringSubmatch(rawShellCommand, -1)
|
||||
@@ -50,9 +54,12 @@ func parseActionArguments(rawShellCommand string, values map[string]string, acti
|
||||
rawShellCommand = strings.ReplaceAll(rawShellCommand, match[0], argValue)
|
||||
}
|
||||
|
||||
rawShellCommand = sv.ReplaceEntityVars(entityPrefix, rawShellCommand)
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"cmd": rawShellCommand,
|
||||
}).Infof("After Parse Args")
|
||||
"actionTitle": actionTitle,
|
||||
"cmd": rawShellCommand,
|
||||
}).Infof("Action parse args - After")
|
||||
|
||||
return rawShellCommand, nil
|
||||
}
|
||||
@@ -64,6 +71,10 @@ func typecheckActionArgument(name string, value string, action *config.Action) e
|
||||
return errors.New("Action arg not defined: " + name)
|
||||
}
|
||||
|
||||
if value == "" {
|
||||
return typecheckNull(arg)
|
||||
}
|
||||
|
||||
if len(arg.Choices) > 0 {
|
||||
return typecheckChoice(value, arg)
|
||||
}
|
||||
@@ -71,7 +82,19 @@ func typecheckActionArgument(name string, value string, action *config.Action) e
|
||||
return TypeSafetyCheck(name, value, arg.Type)
|
||||
}
|
||||
|
||||
func typecheckNull(arg *config.ActionArgument) error {
|
||||
if arg.RejectNull {
|
||||
return errors.New("Null values are not allowed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func typecheckChoice(value string, arg *config.ActionArgument) error {
|
||||
if arg.Entity != "" {
|
||||
return typecheckChoiceEntity(value, arg)
|
||||
}
|
||||
|
||||
for _, choice := range arg.Choices {
|
||||
if value == choice.Value {
|
||||
return nil
|
||||
@@ -81,31 +104,68 @@ func typecheckChoice(value string, arg *config.ActionArgument) error {
|
||||
return errors.New("argument value is not one of the predefined choices")
|
||||
}
|
||||
|
||||
func typecheckChoiceEntity(value string, arg *config.ActionArgument) error {
|
||||
templateChoice := arg.Choices[0].Value
|
||||
|
||||
for _, ent := range sv.GetEntities(arg.Entity) {
|
||||
choice := sv.ReplaceEntityVars(ent, templateChoice)
|
||||
|
||||
if value == choice {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("argument value cannot be found in entities")
|
||||
}
|
||||
|
||||
// TypeSafetyCheck checks argument values match a specific type. The types are
|
||||
// defined in typecheckRegex, and, you guessed it, uses regex to check for allowed
|
||||
// characters.
|
||||
func TypeSafetyCheck(name string, value string, argumentType string) error {
|
||||
if argumentType == "url" {
|
||||
switch argumentType {
|
||||
case "password":
|
||||
return nil
|
||||
case "url":
|
||||
return typeSafetyCheckUrl(name, value)
|
||||
case "datetime":
|
||||
return typeSafetyCheckDatetime(name, value)
|
||||
}
|
||||
|
||||
return typeSafetyCheckRegex(name, value, argumentType)
|
||||
}
|
||||
|
||||
func typeSafetyCheckRegex(name string, value string, argumentType string) error {
|
||||
pattern, found := typecheckRegex[argumentType]
|
||||
func typeSafetyCheckDatetime(name string, value string) error {
|
||||
_, err := time.Parse("2006-01-02T15:04:05", value)
|
||||
|
||||
if !found {
|
||||
return errors.New("argument type not implemented " + argumentType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func typeSafetyCheckRegex(name string, value string, argumentType string) error {
|
||||
pattern := ""
|
||||
|
||||
if strings.HasPrefix(argumentType, "regex:") {
|
||||
pattern = strings.Replace(argumentType, "regex:", "", 1)
|
||||
} else {
|
||||
found := false
|
||||
pattern, found = typecheckRegex[argumentType]
|
||||
|
||||
if !found {
|
||||
return errors.New("argument type not implemented " + argumentType)
|
||||
}
|
||||
}
|
||||
|
||||
matches, _ := regexp.MatchString(pattern, value)
|
||||
|
||||
if !matches {
|
||||
log.WithFields(log.Fields{
|
||||
"name": name,
|
||||
"value": value,
|
||||
"type": argumentType,
|
||||
"name": name,
|
||||
"value": value,
|
||||
"type": argumentType,
|
||||
"pattern": pattern,
|
||||
}).Warn("Arg type check safety failure")
|
||||
|
||||
return errors.New("invalid argument, doesn't match " + argumentType)
|
||||
|
||||
@@ -17,6 +17,34 @@ func TestSanitizeUnimplemented(t *testing.T) {
|
||||
assert.NotNil(t, err, "Test an argument type that does not exist")
|
||||
}
|
||||
|
||||
func TestArgumentValueNullable(t *testing.T) {
|
||||
a1 := config.Action{
|
||||
Title: "Release the hounds",
|
||||
Shell: "echo 'Releasing {{ count }} hounds'",
|
||||
Arguments: []config.ActionArgument{
|
||||
{
|
||||
Name: "count",
|
||||
Type: "int",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
values := map[string]string{
|
||||
"count": "",
|
||||
}
|
||||
|
||||
out, err := parseActionArguments(a1.Shell, values, &a1, a1.Title, "")
|
||||
|
||||
assert.Equal(t, "echo 'Releasing hounds'", out)
|
||||
assert.Nil(t, err)
|
||||
|
||||
a1.Arguments[0].RejectNull = true
|
||||
|
||||
_, err = parseActionArguments(a1.Shell, values, &a1, a1.Title, "")
|
||||
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestArgumentNameNumbers(t *testing.T) {
|
||||
a1 := config.Action{
|
||||
Title: "Do some tickles",
|
||||
@@ -33,7 +61,7 @@ func TestArgumentNameNumbers(t *testing.T) {
|
||||
"person1name": "Fred",
|
||||
}
|
||||
|
||||
out, err := parseActionArguments(a1.Shell, values, &a1)
|
||||
out, err := parseActionArguments(a1.Shell, values, &a1, a1.Title, "")
|
||||
|
||||
assert.Equal(t, "echo 'Tickling Fred'", out)
|
||||
assert.Nil(t, err)
|
||||
@@ -53,7 +81,7 @@ func TestArgumentNotProvided(t *testing.T) {
|
||||
|
||||
values := map[string]string{}
|
||||
|
||||
out, err := parseActionArguments(a1.Shell, values, &a1)
|
||||
out, err := parseActionArguments(a1.Shell, values, &a1, a1.Title, "")
|
||||
|
||||
assert.Equal(t, "", out)
|
||||
assert.Equal(t, err.Error(), "Required arg not provided: personName")
|
||||
|
||||
@@ -1,138 +1,341 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
pb "github.com/OliveTin/OliveTin/gen/grpc"
|
||||
acl "github.com/OliveTin/OliveTin/internal/acl"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
sv "github.com/OliveTin/OliveTin/internal/stringvariables"
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"bytes"
|
||||
"context"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
metricActionsRequested = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "olivetin_actions_requested_count",
|
||||
Help: "The actions requested count",
|
||||
})
|
||||
)
|
||||
|
||||
type ActionBinding struct {
|
||||
Action *config.Action
|
||||
EntityPrefix string
|
||||
ConfigOrder int
|
||||
}
|
||||
|
||||
// Executor represents a helper class for executing commands. It's main method
|
||||
// is ExecRequest
|
||||
type Executor struct {
|
||||
logs map[string]*InternalLogEntry
|
||||
LogsByActionId map[string][]*InternalLogEntry
|
||||
|
||||
logmutex sync.RWMutex
|
||||
|
||||
MapActionIdToBinding map[string]*ActionBinding
|
||||
MapActionIdToBindingLock sync.RWMutex
|
||||
|
||||
Cfg *config.Config
|
||||
|
||||
listeners []listener
|
||||
|
||||
chainOfCommand []executorStepFunc
|
||||
}
|
||||
|
||||
// ExecutionRequest is a request to execute an action. It's passed to an
|
||||
// Executor. They're created from the grpcapi.
|
||||
type ExecutionRequest struct {
|
||||
ActionName string
|
||||
Arguments map[string]string
|
||||
Tags []string
|
||||
action *config.Action
|
||||
Cfg *config.Config
|
||||
AuthenticatedUser *acl.AuthenticatedUser
|
||||
ActionTitle string
|
||||
Action *config.Action
|
||||
Arguments map[string]string
|
||||
TrackingID string
|
||||
Tags []string
|
||||
Cfg *config.Config
|
||||
AuthenticatedUser *acl.AuthenticatedUser
|
||||
EntityPrefix string
|
||||
|
||||
logEntry *InternalLogEntry
|
||||
finalParsedCommand string
|
||||
executor *Executor
|
||||
}
|
||||
|
||||
// InternalLogEntry objects are created by an Executor, and represent the final
|
||||
// state of execution (even if the command is not executed). It's designed to be
|
||||
// easily serializable.
|
||||
type InternalLogEntry struct {
|
||||
Datetime string
|
||||
Stdout string
|
||||
Stderr string
|
||||
TimedOut bool
|
||||
ExitCode int32
|
||||
Tags []string
|
||||
DatetimeStarted time.Time
|
||||
DatetimeFinished time.Time
|
||||
Output string
|
||||
TimedOut bool
|
||||
Blocked bool
|
||||
ExitCode int32
|
||||
Tags []string
|
||||
ExecutionStarted bool
|
||||
ExecutionFinished bool
|
||||
ExecutionTrackingID string
|
||||
Process *os.Process
|
||||
|
||||
/*
|
||||
The following two properties are obviously on Action normally, but it's useful
|
||||
The following 3 properties are obviously on Action normally, but it's useful
|
||||
that logs are lightweight (so we don't need to have an action associated to
|
||||
logs, etc. Therefore, we duplicate those values here.
|
||||
*/
|
||||
ActionTitle string
|
||||
ActionIcon string
|
||||
ActionId string
|
||||
}
|
||||
|
||||
type executorStepFunc func(*ExecutionRequest) bool
|
||||
|
||||
// Executor represents a helper class for executing commands. It's main method
|
||||
// is ExecRequest
|
||||
type Executor struct {
|
||||
Logs []InternalLogEntry
|
||||
// DefaultExecutor returns an Executor, with a sensible "chain of command" for
|
||||
// executing actions.
|
||||
func DefaultExecutor(cfg *config.Config) *Executor {
|
||||
e := Executor{}
|
||||
e.Cfg = cfg
|
||||
e.logs = make(map[string]*InternalLogEntry)
|
||||
e.LogsByActionId = make(map[string][]*InternalLogEntry)
|
||||
e.MapActionIdToBinding = make(map[string]*ActionBinding)
|
||||
|
||||
chainOfCommand []executorStepFunc
|
||||
e.chainOfCommand = []executorStepFunc{
|
||||
stepRequestAction,
|
||||
stepConcurrencyCheck,
|
||||
stepRateCheck,
|
||||
stepACLCheck,
|
||||
stepParseArgs,
|
||||
stepLogStart,
|
||||
stepExec,
|
||||
stepExecAfter,
|
||||
stepLogFinish,
|
||||
stepSaveLog,
|
||||
stepTrigger,
|
||||
}
|
||||
|
||||
return &e
|
||||
}
|
||||
|
||||
type listener interface {
|
||||
OnExecutionStarted(actionTitle string)
|
||||
OnExecutionFinished(logEntry *InternalLogEntry)
|
||||
OnOutputChunk(o []byte, executionTrackingId string)
|
||||
OnActionMapRebuilt()
|
||||
}
|
||||
|
||||
func (e *Executor) AddListener(m listener) {
|
||||
e.listeners = append(e.listeners, m)
|
||||
}
|
||||
|
||||
func (e *Executor) GetLogsCopy() map[string]*InternalLogEntry {
|
||||
e.logmutex.RLock()
|
||||
|
||||
copy := make(map[string]*InternalLogEntry)
|
||||
|
||||
for k, v := range e.logs {
|
||||
copy[k] = v
|
||||
}
|
||||
|
||||
e.logmutex.RUnlock()
|
||||
|
||||
return copy
|
||||
}
|
||||
|
||||
func (e *Executor) GetLog(trackingID string) (*InternalLogEntry, bool) {
|
||||
e.logmutex.RLock()
|
||||
|
||||
entry, found := e.logs[trackingID]
|
||||
|
||||
e.logmutex.RUnlock()
|
||||
|
||||
return entry, found
|
||||
}
|
||||
|
||||
func (e *Executor) GetLogsByActionId(actionId string) []*InternalLogEntry {
|
||||
e.logmutex.RLock()
|
||||
|
||||
logs, found := e.LogsByActionId[actionId]
|
||||
|
||||
e.logmutex.RUnlock()
|
||||
|
||||
if !found {
|
||||
return make([]*InternalLogEntry, 0)
|
||||
}
|
||||
|
||||
return logs
|
||||
}
|
||||
|
||||
func (e *Executor) SetLog(trackingID string, entry *InternalLogEntry) {
|
||||
e.logmutex.Lock()
|
||||
|
||||
e.logs[trackingID] = entry
|
||||
|
||||
e.logmutex.Unlock()
|
||||
}
|
||||
|
||||
// ExecRequest processes an ExecutionRequest
|
||||
func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
|
||||
func (e *Executor) ExecRequest(req *ExecutionRequest) (*sync.WaitGroup, string) {
|
||||
req.executor = e
|
||||
req.logEntry = &InternalLogEntry{
|
||||
Datetime: time.Now().Format("2006-01-02 15:04:05"),
|
||||
ActionTitle: req.ActionName,
|
||||
Stdout: "",
|
||||
Stderr: "",
|
||||
ExitCode: -1337, // If an Action is not actually executed, this is the default exit code.
|
||||
DatetimeStarted: time.Now(),
|
||||
ExecutionTrackingID: req.TrackingID,
|
||||
Output: "",
|
||||
ExitCode: -1337, // If an Action is not actually executed, this is the default exit code.
|
||||
ExecutionStarted: false,
|
||||
ExecutionFinished: false,
|
||||
ActionId: "",
|
||||
ActionTitle: "notfound",
|
||||
ActionIcon: "💩",
|
||||
}
|
||||
|
||||
_, isDuplicate := e.GetLog(req.TrackingID)
|
||||
|
||||
if isDuplicate || req.TrackingID == "" {
|
||||
req.TrackingID = uuid.NewString()
|
||||
}
|
||||
|
||||
log.Tracef("executor.ExecRequest(): %v", req)
|
||||
|
||||
e.SetLog(req.TrackingID, req.logEntry)
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
wg.Add(1)
|
||||
|
||||
go func() {
|
||||
e.execChain(req)
|
||||
defer wg.Done()
|
||||
}()
|
||||
|
||||
return wg, req.TrackingID
|
||||
}
|
||||
|
||||
func (e *Executor) execChain(req *ExecutionRequest) {
|
||||
for _, step := range e.chainOfCommand {
|
||||
if !step(req) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
e.Logs = append(e.Logs, *req.logEntry)
|
||||
req.logEntry.ExecutionFinished = true
|
||||
|
||||
return &pb.StartActionResponse{
|
||||
LogEntry: &pb.LogEntry{
|
||||
ActionTitle: req.logEntry.ActionTitle,
|
||||
ActionIcon: req.logEntry.ActionIcon,
|
||||
Datetime: req.logEntry.Datetime,
|
||||
Stderr: req.logEntry.Stderr,
|
||||
Stdout: req.logEntry.Stdout,
|
||||
TimedOut: req.logEntry.TimedOut,
|
||||
ExitCode: req.logEntry.ExitCode,
|
||||
},
|
||||
}
|
||||
// This isn't a step, because we want to notify all listeners, irrespective
|
||||
// of how many steps were actually executed.
|
||||
notifyListeners(req)
|
||||
}
|
||||
|
||||
// DefaultExecutor returns an Executor, with a sensible "chain of command" for
|
||||
// executing actions.
|
||||
func DefaultExecutor() *Executor {
|
||||
e := Executor{}
|
||||
e.chainOfCommand = []executorStepFunc{
|
||||
stepFindAction,
|
||||
stepACLCheck,
|
||||
stepParseArgs,
|
||||
stepLogStart,
|
||||
stepExec,
|
||||
stepLogFinish,
|
||||
func getConcurrentCount(req *ExecutionRequest) int {
|
||||
concurrentCount := 0
|
||||
|
||||
req.executor.logmutex.RLock()
|
||||
|
||||
for _, log := range req.executor.GetLogsByActionId(req.Action.ID) {
|
||||
if !log.ExecutionFinished {
|
||||
concurrentCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
return &e
|
||||
req.executor.logmutex.RUnlock()
|
||||
|
||||
return concurrentCount
|
||||
}
|
||||
|
||||
func stepFindAction(req *ExecutionRequest) bool {
|
||||
actualAction := req.Cfg.FindAction(req.ActionName)
|
||||
func stepConcurrencyCheck(req *ExecutionRequest) bool {
|
||||
concurrentCount := getConcurrentCount(req)
|
||||
|
||||
// Note that the current execution is counted int the logs, so when checking we +1
|
||||
if concurrentCount >= (req.Action.MaxConcurrent + 1) {
|
||||
msg := fmt.Sprintf("Blocked from executing. This would mean this action is running %d times concurrently, but this action has maxExecutions set to %d.", concurrentCount, req.Action.MaxConcurrent)
|
||||
|
||||
if actualAction == nil {
|
||||
log.WithFields(log.Fields{
|
||||
"actionName": req.ActionName,
|
||||
}).Warnf("Action not found")
|
||||
|
||||
req.logEntry.Stderr = "Action not found"
|
||||
"actionTitle": req.logEntry.ActionTitle,
|
||||
}).Warnf(msg)
|
||||
|
||||
req.logEntry.Output = msg
|
||||
req.logEntry.Blocked = true
|
||||
return false
|
||||
}
|
||||
|
||||
req.action = actualAction
|
||||
req.logEntry.ActionIcon = actualAction.Icon
|
||||
return true
|
||||
}
|
||||
|
||||
func parseDuration(rate config.RateSpec) time.Duration {
|
||||
duration, err := time.ParseDuration(rate.Duration)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Could not parse duration: %v", rate.Duration)
|
||||
|
||||
return -1 * time.Minute
|
||||
}
|
||||
|
||||
return duration
|
||||
}
|
||||
|
||||
func getExecutionsCount(rate config.RateSpec, req *ExecutionRequest) int {
|
||||
executions := -1 // Because we will find ourself when checking execution logs
|
||||
|
||||
duration := parseDuration(rate)
|
||||
|
||||
then := time.Now().Add(-duration)
|
||||
|
||||
for _, logEntry := range req.executor.GetLogsByActionId(req.Action.ID) {
|
||||
if logEntry.DatetimeStarted.After(then) && !logEntry.Blocked {
|
||||
|
||||
executions += 1
|
||||
}
|
||||
}
|
||||
|
||||
return executions
|
||||
}
|
||||
|
||||
func stepRateCheck(req *ExecutionRequest) bool {
|
||||
for _, rate := range req.Action.MaxRate {
|
||||
executions := getExecutionsCount(rate, req)
|
||||
|
||||
if executions >= rate.Limit {
|
||||
msg := fmt.Sprintf("Blocked from executing. This action has run %d out of %d allowed times in the last %s.", executions, rate.Limit, rate.Duration)
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"actionTitle": req.logEntry.ActionTitle,
|
||||
}).Infof(msg)
|
||||
|
||||
req.logEntry.Output = msg
|
||||
req.logEntry.Blocked = true
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func stepACLCheck(req *ExecutionRequest) bool {
|
||||
return acl.IsAllowedExec(req.Cfg, req.AuthenticatedUser, req.action)
|
||||
canExec := acl.IsAllowedExec(req.Cfg, req.AuthenticatedUser, req.Action)
|
||||
|
||||
if !canExec {
|
||||
req.logEntry.Output = "ACL check failed. Blocked from executing."
|
||||
req.logEntry.Blocked = true
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"actionTitle": req.logEntry.ActionTitle,
|
||||
}).Warnf("ACL check failed. Blocked from executing.")
|
||||
}
|
||||
|
||||
return canExec
|
||||
}
|
||||
|
||||
func stepParseArgs(req *ExecutionRequest) bool {
|
||||
var err error
|
||||
|
||||
req.finalParsedCommand, err = parseActionArguments(req.action.Shell, req.Arguments, req.action)
|
||||
req.finalParsedCommand, err = parseActionArguments(req.Action.Shell, req.Arguments, req.Action, req.logEntry.ActionTitle, req.EntityPrefix)
|
||||
|
||||
if err != nil {
|
||||
req.logEntry.Stdout = err.Error()
|
||||
req.logEntry.Output = err.Error()
|
||||
|
||||
log.Warnf(err.Error())
|
||||
|
||||
@@ -142,61 +345,270 @@ func stepParseArgs(req *ExecutionRequest) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func stepRequestAction(req *ExecutionRequest) bool {
|
||||
// The grpc API always tries to find the action by ID, but it may
|
||||
if req.Action == nil {
|
||||
log.WithFields(log.Fields{
|
||||
"actionTitle": req.ActionTitle,
|
||||
}).Infof("Action finding by title")
|
||||
|
||||
req.Action = req.Cfg.FindAction(req.ActionTitle)
|
||||
|
||||
if req.Action == nil {
|
||||
log.WithFields(log.Fields{
|
||||
"actionTitle": req.ActionTitle,
|
||||
}).Warnf("Action requested, but not found")
|
||||
|
||||
req.logEntry.Output = "Action not found: " + req.ActionTitle
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
metricActionsRequested.Inc()
|
||||
|
||||
req.logEntry.ActionTitle = sv.ReplaceEntityVars(req.EntityPrefix, req.Action.Title)
|
||||
req.logEntry.ActionIcon = req.Action.Icon
|
||||
req.logEntry.ActionId = req.Action.ID
|
||||
req.logEntry.Tags = req.Tags
|
||||
|
||||
req.executor.logmutex.Lock()
|
||||
|
||||
if _, containsKey := req.executor.LogsByActionId[req.Action.ID]; !containsKey {
|
||||
req.executor.LogsByActionId[req.Action.ID] = make([]*InternalLogEntry, 0)
|
||||
}
|
||||
|
||||
req.executor.LogsByActionId[req.Action.ID] = append(req.executor.LogsByActionId[req.Action.ID], req.logEntry)
|
||||
|
||||
req.executor.logmutex.Unlock()
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"actionTitle": req.logEntry.ActionTitle,
|
||||
"tags": req.Tags,
|
||||
}).Infof("Action requested")
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func stepLogStart(req *ExecutionRequest) bool {
|
||||
log.WithFields(log.Fields{
|
||||
"title": req.action.Title,
|
||||
"timeout": req.action.Timeout,
|
||||
}).Infof("Action starting")
|
||||
"actionTitle": req.logEntry.ActionTitle,
|
||||
"timeout": req.Action.Timeout,
|
||||
}).Infof("Action started")
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func stepLogFinish(req *ExecutionRequest) bool {
|
||||
req.logEntry.ExecutionFinished = true
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"title": req.action.Title,
|
||||
"stdout": req.logEntry.Stdout,
|
||||
"stderr": req.logEntry.Stderr,
|
||||
"timedOut": req.logEntry.TimedOut,
|
||||
"exit": req.logEntry.ExitCode,
|
||||
"actionTitle": req.logEntry.ActionTitle,
|
||||
"outputLength": len(req.logEntry.Output),
|
||||
"timedOut": req.logEntry.TimedOut,
|
||||
"exit": req.logEntry.ExitCode,
|
||||
}).Infof("Action finished")
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cmd {
|
||||
if runtime.GOOS == "windows" {
|
||||
return exec.CommandContext(ctx, "cmd", "/C", finalParsedCommand)
|
||||
func notifyListeners(req *ExecutionRequest) {
|
||||
for _, listener := range req.executor.listeners {
|
||||
listener.OnExecutionFinished(req.logEntry)
|
||||
}
|
||||
}
|
||||
|
||||
func appendErrorToStderr(err error, logEntry *InternalLogEntry) {
|
||||
if err != nil {
|
||||
logEntry.Output = err.Error() + "\n\n" + logEntry.Output
|
||||
}
|
||||
}
|
||||
|
||||
type OutputStreamer struct {
|
||||
Req *ExecutionRequest
|
||||
output bytes.Buffer
|
||||
}
|
||||
|
||||
func (ost *OutputStreamer) Write(o []byte) (n int, err error) {
|
||||
for _, listener := range ost.Req.executor.listeners {
|
||||
listener.OnOutputChunk(o, ost.Req.TrackingID)
|
||||
}
|
||||
|
||||
return exec.CommandContext(ctx, "sh", "-c", finalParsedCommand)
|
||||
return ost.output.Write(o)
|
||||
}
|
||||
|
||||
func (ost *OutputStreamer) String() string {
|
||||
return ost.output.String()
|
||||
}
|
||||
|
||||
func buildEnv(req *ExecutionRequest) []string {
|
||||
ret := append(os.Environ(), "OLIVETIN=1")
|
||||
|
||||
for k, v := range req.Arguments {
|
||||
varName := fmt.Sprintf("%v", strings.TrimSpace(strings.ToUpper(k)))
|
||||
|
||||
// Skip arguments that might not have a name (eg, confirmation), as this causes weird bugs on Windows.
|
||||
if varName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
ret = append(ret, fmt.Sprintf("%v=%v", varName, v))
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func stepExec(req *ExecutionRequest) bool {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.action.Timeout)*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.Action.Timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
streamer := &OutputStreamer{Req: req}
|
||||
|
||||
cmd := wrapCommandInShell(ctx, req.finalParsedCommand)
|
||||
cmd.Stdout = streamer
|
||||
cmd.Stderr = streamer
|
||||
cmd.Env = buildEnv(req)
|
||||
|
||||
req.logEntry.ExecutionStarted = true
|
||||
|
||||
runerr := cmd.Start()
|
||||
|
||||
req.logEntry.Process = cmd.Process
|
||||
|
||||
waiterr := cmd.Wait()
|
||||
|
||||
req.logEntry.ExitCode = int32(cmd.ProcessState.ExitCode())
|
||||
req.logEntry.Output = streamer.String()
|
||||
|
||||
appendErrorToStderr(runerr, req.logEntry)
|
||||
appendErrorToStderr(waiterr, req.logEntry)
|
||||
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
log.WithFields(log.Fields{
|
||||
"actionTitle": req.logEntry.ActionTitle,
|
||||
}).Warnf("Action timed out")
|
||||
|
||||
// The context timeout should kill the process, but let's make sure.
|
||||
req.executor.Kill(req.logEntry)
|
||||
req.logEntry.TimedOut = true
|
||||
req.logEntry.Output += "OliveTin::timeout - this action timed out after " + fmt.Sprintf("%v", req.Action.Timeout) + " seconds. If you need more time for this action, set a longer timeout. See https://docs.olivetin.app/timeout.html for more help."
|
||||
}
|
||||
|
||||
req.logEntry.DatetimeFinished = time.Now()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func stepExecAfter(req *ExecutionRequest) bool {
|
||||
if req.Action.ShellAfterCompleted == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.Action.Timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
cmd := wrapCommandInShell(ctx, req.finalParsedCommand)
|
||||
args := map[string]string{
|
||||
"output": req.logEntry.Output,
|
||||
"exitCode": fmt.Sprintf("%v", req.logEntry.ExitCode),
|
||||
}
|
||||
|
||||
finalParsedCommand, _ := parseActionArguments(req.Action.ShellAfterCompleted, args, req.Action, req.logEntry.ActionTitle, req.EntityPrefix)
|
||||
|
||||
cmd := wrapCommandInShell(ctx, finalParsedCommand)
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
runerr := cmd.Run()
|
||||
runerr := cmd.Start()
|
||||
|
||||
req.logEntry.ExitCode = int32(cmd.ProcessState.ExitCode())
|
||||
req.logEntry.Stdout = stdout.String()
|
||||
req.logEntry.Stderr = stderr.String()
|
||||
waiterr := cmd.Wait()
|
||||
|
||||
if runerr != nil {
|
||||
req.logEntry.Stderr = runerr.Error() + "\n\n" + req.logEntry.Stderr
|
||||
}
|
||||
req.logEntry.Output += "\n" + stdout.String()
|
||||
req.logEntry.Output += "OliveTin::shellAfterCompleted stdout\n" + stdout.String()
|
||||
req.logEntry.Output += stdout.String()
|
||||
|
||||
req.logEntry.Output += "OliveTin::shellAfterCompleted stderr\n" + stdout.String()
|
||||
req.logEntry.Output += stderr.String()
|
||||
|
||||
req.logEntry.Output += "OliveTin::shellAfterCompleted errors and summary\n" + stdout.String()
|
||||
appendErrorToStderr(runerr, req.logEntry)
|
||||
appendErrorToStderr(waiterr, req.logEntry)
|
||||
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
req.logEntry.TimedOut = true
|
||||
req.logEntry.Output += "Your shellAfterCompleted command timed out."
|
||||
}
|
||||
|
||||
req.logEntry.Tags = req.Tags
|
||||
req.logEntry.Output += fmt.Sprintf("Your shellAfterCompleted exited with code %v\n", cmd.ProcessState.ExitCode())
|
||||
|
||||
req.logEntry.Output += "OliveTin::shellAfterCompleted output complete\n" + stdout.String()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func stepTrigger(req *ExecutionRequest) bool {
|
||||
if req.Action.Trigger != "" {
|
||||
trigger := &ExecutionRequest{
|
||||
ActionTitle: req.Action.Trigger,
|
||||
TrackingID: uuid.NewString(),
|
||||
Tags: []string{"trigger"},
|
||||
AuthenticatedUser: req.AuthenticatedUser,
|
||||
Cfg: req.Cfg,
|
||||
}
|
||||
|
||||
req.executor.ExecRequest(trigger)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func stepSaveLog(req *ExecutionRequest) bool {
|
||||
filename := fmt.Sprintf("%v.%v.%v", req.logEntry.ActionTitle, req.logEntry.DatetimeStarted.Unix(), req.logEntry.ExecutionTrackingID)
|
||||
|
||||
saveLogResults(req, filename)
|
||||
saveLogOutput(req, filename)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func firstNonEmpty(one, two string) string {
|
||||
if one != "" {
|
||||
return one
|
||||
}
|
||||
|
||||
return two
|
||||
}
|
||||
|
||||
func saveLogResults(req *ExecutionRequest, filename string) {
|
||||
dir := firstNonEmpty(req.Action.SaveLogs.ResultsDirectory, req.Cfg.SaveLogs.ResultsDirectory)
|
||||
|
||||
if dir != "" {
|
||||
data, err := yaml.Marshal(req.logEntry)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("%v", err)
|
||||
}
|
||||
|
||||
filepath := path.Join(dir, filename+".yaml")
|
||||
err = os.WriteFile(filepath, data, 0644)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("%v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func saveLogOutput(req *ExecutionRequest, filename string) {
|
||||
dir := firstNonEmpty(req.Action.SaveLogs.OutputDirectory, req.Cfg.SaveLogs.OutputDirectory)
|
||||
|
||||
if dir != "" {
|
||||
data := req.logEntry.Output
|
||||
filepath := path.Join(dir, filename+".log")
|
||||
err := os.WriteFile(filepath, []byte(data), 0644)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("%v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
89
internal/executor/executor_actions.go
Normal file
89
internal/executor/executor_actions.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
sv "github.com/OliveTin/OliveTin/internal/stringvariables"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func (e *Executor) FindActionBindingByID(id string) *config.Action {
|
||||
e.MapActionIdToBindingLock.RLock()
|
||||
pair, found := e.MapActionIdToBinding[id]
|
||||
e.MapActionIdToBindingLock.RUnlock()
|
||||
|
||||
if found {
|
||||
log.Infof("findActionBinding %v, %v", id, pair.Action.ID)
|
||||
return pair.Action
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Executor) RebuildActionMap() {
|
||||
e.MapActionIdToBindingLock.Lock()
|
||||
|
||||
clear(e.MapActionIdToBinding)
|
||||
|
||||
for configOrder, action := range e.Cfg.Actions {
|
||||
if action.Entity != "" {
|
||||
registerActionsFromEntities(e, configOrder, action.Entity, action)
|
||||
} else {
|
||||
registerAction(e, configOrder, action)
|
||||
}
|
||||
}
|
||||
|
||||
e.MapActionIdToBindingLock.Unlock()
|
||||
|
||||
for _, l := range e.listeners {
|
||||
l.OnActionMapRebuilt()
|
||||
}
|
||||
}
|
||||
|
||||
func registerAction(e *Executor, configOrder int, action *config.Action) {
|
||||
actionId := hashActionToID(action, "")
|
||||
|
||||
e.MapActionIdToBinding[actionId] = &ActionBinding{
|
||||
Action: action,
|
||||
EntityPrefix: "noent",
|
||||
ConfigOrder: configOrder,
|
||||
}
|
||||
}
|
||||
|
||||
func registerActionsFromEntities(e *Executor, configOrder int, entityTitle string, tpl *config.Action) {
|
||||
entityCount, _ := strconv.Atoi(sv.Get("entities." + entityTitle + ".count"))
|
||||
|
||||
for i := 0; i < entityCount; i++ {
|
||||
registerActionFromEntity(e, configOrder, tpl, entityTitle, i)
|
||||
}
|
||||
}
|
||||
|
||||
func registerActionFromEntity(e *Executor, configOrder int, tpl *config.Action, entityTitle string, entityIndex int) {
|
||||
prefix := sv.GetEntityPrefix(entityTitle, entityIndex)
|
||||
|
||||
virtualActionId := hashActionToID(tpl, prefix)
|
||||
|
||||
e.MapActionIdToBinding[virtualActionId] = &ActionBinding{
|
||||
Action: tpl,
|
||||
EntityPrefix: prefix,
|
||||
ConfigOrder: configOrder,
|
||||
}
|
||||
}
|
||||
|
||||
func hashActionToID(action *config.Action, entityPrefix string) string {
|
||||
if action.ID != "" && entityPrefix == "" {
|
||||
return action.ID
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
|
||||
if entityPrefix == "" {
|
||||
h.Write([]byte(action.Title))
|
||||
} else {
|
||||
h.Write([]byte(action.ID + "." + entityPrefix))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
)
|
||||
|
||||
func testingExecutor() (*Executor, *config.Config) {
|
||||
e := DefaultExecutor()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
|
||||
a1 := config.Action{
|
||||
e := DefaultExecutor(cfg)
|
||||
|
||||
a1 := &config.Action{
|
||||
Title: "Do some tickles",
|
||||
Shell: "echo 'Tickling {{ person }}'",
|
||||
Arguments: []config.ActionArgument{
|
||||
@@ -34,7 +34,7 @@ func TestCreateExecutorAndExec(t *testing.T) {
|
||||
e, cfg := testingExecutor()
|
||||
|
||||
req := ExecutionRequest{
|
||||
ActionName: "Do some tickles",
|
||||
ActionTitle: "Do some tickles",
|
||||
AuthenticatedUser: &acl.AuthenticatedUser{Username: "Mr Tickle"},
|
||||
Cfg: cfg,
|
||||
Arguments: map[string]string{
|
||||
@@ -42,11 +42,11 @@ func TestCreateExecutorAndExec(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
e.ExecRequest(&req)
|
||||
|
||||
assert.NotNil(t, e, "Create an executor")
|
||||
|
||||
assert.NotNil(t, e.ExecRequest(&req), "Execute a request")
|
||||
wg, _ := e.ExecRequest(&req)
|
||||
wg.Wait()
|
||||
|
||||
assert.Equal(t, int32(0), req.logEntry.ExitCode, "Exit code is zero")
|
||||
}
|
||||
|
||||
@@ -54,19 +54,20 @@ func TestExecNonExistant(t *testing.T) {
|
||||
e, cfg := testingExecutor()
|
||||
|
||||
req := ExecutionRequest{
|
||||
ActionName: "Waffles",
|
||||
logEntry: &InternalLogEntry{},
|
||||
Cfg: cfg,
|
||||
ActionTitle: "Waffles",
|
||||
logEntry: &InternalLogEntry{},
|
||||
Cfg: cfg,
|
||||
}
|
||||
|
||||
e.ExecRequest(&req)
|
||||
wg, _ := e.ExecRequest(&req)
|
||||
wg.Wait()
|
||||
|
||||
assert.Equal(t, int32(-1337), req.logEntry.ExitCode, "Log entry is set to an internal error code")
|
||||
assert.Equal(t, "", req.logEntry.ActionIcon, "Log entry icon wasnt found")
|
||||
assert.Equal(t, "💩", req.logEntry.ActionIcon, "Log entry icon is a poop (not found)")
|
||||
}
|
||||
|
||||
func TestArgumentNameCamelCase(t *testing.T) {
|
||||
a1 := config.Action{
|
||||
a1 := &config.Action{
|
||||
Title: "Do some tickles",
|
||||
Shell: "echo 'Tickling {{ personName }}'",
|
||||
Arguments: []config.ActionArgument{
|
||||
@@ -81,14 +82,14 @@ func TestArgumentNameCamelCase(t *testing.T) {
|
||||
"personName": "Fred",
|
||||
}
|
||||
|
||||
out, err := parseActionArguments(a1.Shell, values, &a1)
|
||||
out, err := parseActionArguments(a1.Shell, values, a1, a1.Title, "")
|
||||
|
||||
assert.Equal(t, "echo 'Tickling Fred'", out)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestArgumentNameSnakeCase(t *testing.T) {
|
||||
a1 := config.Action{
|
||||
a1 := &config.Action{
|
||||
Title: "Do some tickles",
|
||||
Shell: "echo 'Tickling {{ person_name }}'",
|
||||
Arguments: []config.ActionArgument{
|
||||
@@ -103,7 +104,7 @@ func TestArgumentNameSnakeCase(t *testing.T) {
|
||||
"person_name": "Fred",
|
||||
}
|
||||
|
||||
out, err := parseActionArguments(a1.Shell, values, &a1)
|
||||
out, err := parseActionArguments(a1.Shell, values, a1, a1.Title, "")
|
||||
|
||||
assert.Equal(t, "echo 'Tickling Fred'", out)
|
||||
assert.Nil(t, err)
|
||||
|
||||
25
internal/executor/executor_unix.go
Normal file
25
internal/executor/executor_unix.go
Normal file
@@ -0,0 +1,25 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package executor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func (e *Executor) Kill(execReq *InternalLogEntry) error {
|
||||
// A negative PID means to kill the whole process group. This is *nix specific behavior.
|
||||
return syscall.Kill(-execReq.Process.Pid, syscall.SIGKILL)
|
||||
}
|
||||
|
||||
func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cmd {
|
||||
cmd := exec.CommandContext(ctx, "sh", "-c", finalParsedCommand)
|
||||
|
||||
// This is to ensure that the process group is killed when the parent process is killed.
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
|
||||
return cmd
|
||||
|
||||
}
|
||||
17
internal/executor/executor_windows.go
Normal file
17
internal/executor/executor_windows.go
Normal file
@@ -0,0 +1,17 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package executor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func (e *Executor) Kill(execReq *InternalLogEntry) error {
|
||||
return execReq.Process.Kill()
|
||||
}
|
||||
|
||||
func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cmd {
|
||||
return exec.CommandContext(ctx, "cmd", "/C", finalParsedCommand)
|
||||
}
|
||||
169
internal/filehelper/file_change_notify.go
Normal file
169
internal/filehelper/file_change_notify.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package filehelper
|
||||
|
||||
import (
|
||||
"github.com/fsnotify/fsnotify"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
debounceWriteLog map[string]*FsNotifyLogEntry
|
||||
|
||||
debounceWriteLogMutex = sync.Mutex{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
debounceWriteLog = make(map[string]*FsNotifyLogEntry)
|
||||
}
|
||||
|
||||
type FsNotifyLogEntry struct {
|
||||
callbackWrapper *time.Timer
|
||||
callbackComplete bool
|
||||
}
|
||||
|
||||
const (
|
||||
debounceDelay = 300 * time.Millisecond
|
||||
)
|
||||
|
||||
type watchContext struct {
|
||||
filename string
|
||||
filedir string
|
||||
callback func(filename string)
|
||||
interestedEvent fsnotify.Op
|
||||
event *fsnotify.Event
|
||||
}
|
||||
|
||||
func WatchDirectoryCreate(fullpath string, callback func(filename string)) {
|
||||
watchPath(&watchContext{
|
||||
filedir: fullpath,
|
||||
filename: "",
|
||||
callback: callback,
|
||||
interestedEvent: fsnotify.Create,
|
||||
})
|
||||
}
|
||||
|
||||
func WatchDirectoryWrite(fullpath string, callback func(filename string)) {
|
||||
watchPath(&watchContext{
|
||||
filedir: fullpath,
|
||||
filename: "",
|
||||
callback: callback,
|
||||
interestedEvent: fsnotify.Write,
|
||||
})
|
||||
}
|
||||
|
||||
func WatchFileWrite(fullpath string, callback func(filename string)) {
|
||||
filename := filepath.Base(fullpath)
|
||||
filedir := filepath.Dir(fullpath)
|
||||
|
||||
watchPath(&watchContext{
|
||||
filedir: filedir,
|
||||
filename: filename,
|
||||
callback: callback,
|
||||
interestedEvent: fsnotify.Write,
|
||||
})
|
||||
}
|
||||
|
||||
func watchPath(ctx *watchContext) {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Could not watch for files being created: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer watcher.Close()
|
||||
|
||||
done := make(chan bool)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
processEvent(ctx, watcher)
|
||||
}
|
||||
}()
|
||||
|
||||
err = watcher.Add(ctx.filedir)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Could not create watcher: %v", err)
|
||||
}
|
||||
|
||||
<-done
|
||||
}
|
||||
|
||||
func processEvent(ctx *watchContext, watcher *fsnotify.Watcher) {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
ctx.event = &event
|
||||
|
||||
if !consumeEvent(ok, ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
break
|
||||
case err := <-watcher.Errors:
|
||||
log.Errorf("Error in fsnotify: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func consumeEvent(ok bool, ctx *watchContext) bool {
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if ctx.filename != "" && filepath.Base(ctx.event.Name) != ctx.filename {
|
||||
log.Tracef("fsnotify irreleventa event different file %+v", ctx.event)
|
||||
return true
|
||||
}
|
||||
|
||||
consumeRelevantEvents(ctx)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func consumeRelevantEvents(ctx *watchContext) {
|
||||
if ctx.event.Has(ctx.interestedEvent) {
|
||||
log.Debugf("fsnotify event relevant: %v", ctx.event)
|
||||
|
||||
processDebounce(ctx)
|
||||
} else {
|
||||
log.Debugf("fsnotify event irrelevant: %v", ctx.event)
|
||||
}
|
||||
}
|
||||
|
||||
func processDebounce(ctx *watchContext) {
|
||||
debounceWriteLogMutex.Lock()
|
||||
|
||||
logEntry, found := debounceWriteLog[ctx.filename]
|
||||
|
||||
if !found {
|
||||
logEntry = &FsNotifyLogEntry{
|
||||
callbackComplete: false,
|
||||
callbackWrapper: nil,
|
||||
}
|
||||
|
||||
debounceWriteLog[ctx.filename] = logEntry
|
||||
}
|
||||
|
||||
log.Debugf("fsnotify event %+v", logEntry)
|
||||
|
||||
if logEntry.callbackComplete || logEntry.callbackWrapper == nil {
|
||||
log.Debugf("fsnotify event callback queued within debounce delay: %v", ctx.filename)
|
||||
|
||||
logEntry.callbackComplete = false
|
||||
logEntry.callbackWrapper = time.AfterFunc(debounceDelay, func() {
|
||||
log.Debugf("fsnotify event callback being fired: %v", ctx.filename)
|
||||
|
||||
ctx.callback(ctx.event.Name)
|
||||
|
||||
logEntry.callbackComplete = true
|
||||
})
|
||||
} else {
|
||||
log.Debugf("fsnotify event suppressed because it's within the debounce delay: %v", ctx.filename)
|
||||
}
|
||||
|
||||
debounceWriteLogMutex.Unlock()
|
||||
}
|
||||
20
internal/filehelper/file_touch.go
Normal file
20
internal/filehelper/file_touch.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package filehelper
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"os"
|
||||
)
|
||||
|
||||
func Touch(filename string, description string) {
|
||||
_, err := os.Stat(filename)
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
_, err := os.Create(filename)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Could not create %v: %v", description, filename)
|
||||
} else {
|
||||
log.Infof("Created %v: %v", description, filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,20 @@ package grpcapi
|
||||
import (
|
||||
ctx "context"
|
||||
pb "github.com/OliveTin/OliveTin/gen/grpc"
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/genproto/googleapis/api/httpbody"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"errors"
|
||||
"net"
|
||||
"sort"
|
||||
|
||||
acl "github.com/OliveTin/OliveTin/internal/acl"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
executor "github.com/OliveTin/OliveTin/internal/executor"
|
||||
installationinfo "github.com/OliveTin/OliveTin/internal/installationinfo"
|
||||
sv "github.com/OliveTin/OliveTin/internal/stringvariables"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -19,62 +24,264 @@ var (
|
||||
)
|
||||
|
||||
type oliveTinAPI struct {
|
||||
pb.UnimplementedOliveTinApiServer
|
||||
// Uncomment this if you want to allow undefined methods during dev.
|
||||
// pb.UnimplementedOliveTinApiServiceServer
|
||||
|
||||
executor *executor.Executor
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) KillAction(ctx ctx.Context, req *pb.KillActionRequest) (*pb.KillActionResponse, error) {
|
||||
ret := &pb.KillActionResponse{
|
||||
ExecutionTrackingId: req.ExecutionTrackingId,
|
||||
}
|
||||
|
||||
execReqLogEntry, found := api.executor.GetLog(req.ExecutionTrackingId)
|
||||
|
||||
ret.Found = found
|
||||
|
||||
if found {
|
||||
log.Warnf("Killing execution request by tracking ID: %v", req.ExecutionTrackingId)
|
||||
|
||||
err := api.executor.Kill(execReqLogEntry)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Killing execution request err: %v", err)
|
||||
ret.AlreadyCompleted = true
|
||||
ret.Killed = false
|
||||
} else {
|
||||
ret.Killed = true
|
||||
}
|
||||
} else {
|
||||
log.Warnf("Killing execution request not possible - not found by tracking ID: %v", req.ExecutionTrackingId)
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *pb.StartActionRequest) (*pb.StartActionResponse, error) {
|
||||
args := make(map[string]string)
|
||||
|
||||
log.Debugf("SA %v", req)
|
||||
|
||||
for _, arg := range req.Arguments {
|
||||
args[arg.Name] = arg.Value
|
||||
}
|
||||
|
||||
api.executor.MapActionIdToBindingLock.RLock()
|
||||
pair := api.executor.MapActionIdToBinding[req.ActionId]
|
||||
api.executor.MapActionIdToBindingLock.RUnlock()
|
||||
|
||||
execReq := executor.ExecutionRequest{
|
||||
ActionName: req.ActionName,
|
||||
Action: pair.Action,
|
||||
EntityPrefix: pair.EntityPrefix,
|
||||
TrackingID: req.UniqueTrackingId,
|
||||
Arguments: args,
|
||||
AuthenticatedUser: acl.UserFromContext(ctx, cfg),
|
||||
Cfg: cfg,
|
||||
}
|
||||
|
||||
return api.executor.ExecRequest(&execReq), nil
|
||||
api.executor.ExecRequest(&execReq)
|
||||
|
||||
return &pb.StartActionResponse{
|
||||
ExecutionTrackingId: execReq.TrackingID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *pb.StartActionAndWaitRequest) (*pb.StartActionAndWaitResponse, error) {
|
||||
args := make(map[string]string)
|
||||
|
||||
execReq := executor.ExecutionRequest{
|
||||
Action: api.executor.FindActionBindingByID(req.ActionId),
|
||||
TrackingID: uuid.NewString(),
|
||||
Arguments: args,
|
||||
AuthenticatedUser: acl.UserFromContext(ctx, cfg),
|
||||
Cfg: cfg,
|
||||
}
|
||||
|
||||
wg, _ := api.executor.ExecRequest(&execReq)
|
||||
wg.Wait()
|
||||
|
||||
internalLogEntry, ok := api.executor.GetLog(execReq.TrackingID)
|
||||
|
||||
if ok {
|
||||
return &pb.StartActionAndWaitResponse{
|
||||
LogEntry: internalLogEntryToPb(internalLogEntry),
|
||||
}, nil
|
||||
} else {
|
||||
return nil, errors.New("Execution not found!")
|
||||
}
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) StartActionByGet(ctx ctx.Context, req *pb.StartActionByGetRequest) (*pb.StartActionByGetResponse, error) {
|
||||
args := make(map[string]string)
|
||||
|
||||
execReq := executor.ExecutionRequest{
|
||||
Action: api.executor.FindActionBindingByID(req.ActionId),
|
||||
TrackingID: uuid.NewString(),
|
||||
Arguments: args,
|
||||
AuthenticatedUser: acl.UserFromContext(ctx, cfg),
|
||||
Cfg: cfg,
|
||||
}
|
||||
|
||||
_, uniqueTrackingId := api.executor.ExecRequest(&execReq)
|
||||
|
||||
return &pb.StartActionByGetResponse{
|
||||
ExecutionTrackingId: uniqueTrackingId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *pb.StartActionByGetAndWaitRequest) (*pb.StartActionByGetAndWaitResponse, error) {
|
||||
args := make(map[string]string)
|
||||
|
||||
execReq := executor.ExecutionRequest{
|
||||
Action: api.executor.FindActionBindingByID(req.ActionId),
|
||||
TrackingID: uuid.NewString(),
|
||||
Arguments: args,
|
||||
AuthenticatedUser: acl.UserFromContext(ctx, cfg),
|
||||
Cfg: cfg,
|
||||
}
|
||||
|
||||
wg, _ := api.executor.ExecRequest(&execReq)
|
||||
wg.Wait()
|
||||
|
||||
internalLogEntry, ok := api.executor.GetLog(execReq.TrackingID)
|
||||
|
||||
if ok {
|
||||
return &pb.StartActionByGetAndWaitResponse{
|
||||
LogEntry: internalLogEntryToPb(internalLogEntry),
|
||||
}, nil
|
||||
} else {
|
||||
return nil, errors.New("Execution not found!")
|
||||
}
|
||||
}
|
||||
|
||||
func internalLogEntryToPb(logEntry *executor.InternalLogEntry) *pb.LogEntry {
|
||||
return &pb.LogEntry{
|
||||
ActionTitle: logEntry.ActionTitle,
|
||||
ActionIcon: logEntry.ActionIcon,
|
||||
ActionId: logEntry.ActionId,
|
||||
DatetimeStarted: logEntry.DatetimeStarted.Format("2006-01-02 15:04:05"),
|
||||
DatetimeFinished: logEntry.DatetimeFinished.Format("2006-01-02 15:04:05"),
|
||||
Output: logEntry.Output,
|
||||
TimedOut: logEntry.TimedOut,
|
||||
Blocked: logEntry.Blocked,
|
||||
ExitCode: logEntry.ExitCode,
|
||||
Tags: logEntry.Tags,
|
||||
ExecutionTrackingId: logEntry.ExecutionTrackingID,
|
||||
ExecutionStarted: logEntry.ExecutionStarted,
|
||||
ExecutionFinished: logEntry.ExecutionFinished,
|
||||
}
|
||||
}
|
||||
|
||||
func getExecutionStatusByTrackingID(api *oliveTinAPI, executionTrackingId string) *executor.InternalLogEntry {
|
||||
logEntry, ok := api.executor.GetLog(executionTrackingId)
|
||||
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return logEntry
|
||||
}
|
||||
|
||||
func getMostRecentExecutionStatusById(api *oliveTinAPI, actionId string) *executor.InternalLogEntry {
|
||||
var ile *executor.InternalLogEntry
|
||||
|
||||
logs := api.executor.GetLogsByActionId(actionId)
|
||||
|
||||
if len(logs) == 0 {
|
||||
return nil
|
||||
} else {
|
||||
// Get last log entry
|
||||
ile = logs[len(logs)-1]
|
||||
}
|
||||
|
||||
return ile
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *pb.ExecutionStatusRequest) (*pb.ExecutionStatusResponse, error) {
|
||||
res := &pb.ExecutionStatusResponse{}
|
||||
|
||||
var ile *executor.InternalLogEntry
|
||||
|
||||
if req.ExecutionTrackingId != "" {
|
||||
ile = getExecutionStatusByTrackingID(api, req.ExecutionTrackingId)
|
||||
} else {
|
||||
ile = getMostRecentExecutionStatusById(api, req.ActionId)
|
||||
}
|
||||
|
||||
res.LogEntry = internalLogEntryToPb(ile)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
/**
|
||||
func (api *oliveTinAPI) WatchExecution(req *pb.WatchExecutionRequest, srv pb.OliveTinApi_WatchExecutionServer) error {
|
||||
log.Infof("Watch")
|
||||
|
||||
if logEntry, ok := api.executor.Logs[req.ExecutionUuid]; !ok {
|
||||
log.Errorf("Execution not found: %v", req.ExecutionUuid)
|
||||
|
||||
return nil
|
||||
} else {
|
||||
if logEntry.ExecutionStarted {
|
||||
for !logEntry.ExecutionCompleted {
|
||||
tmp := make([]byte, 256)
|
||||
|
||||
red, err := io.ReadAtLeast(logEntry.StdoutBuffer, tmp, 1)
|
||||
|
||||
log.Infof("%v %v", red, err)
|
||||
|
||||
srv.Send(&pb.WatchExecutionUpdate{
|
||||
Update: string(tmp),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
func (api *oliveTinAPI) GetDashboardComponents(ctx ctx.Context, req *pb.GetDashboardComponentsRequest) (*pb.GetDashboardComponentsResponse, error) {
|
||||
user := acl.UserFromContext(ctx, cfg)
|
||||
|
||||
res := actionsCfgToPb(cfg.Actions, user)
|
||||
res := buildDashboardResponse(api.executor, cfg, user)
|
||||
|
||||
if len(res.Actions) == 0 {
|
||||
log.Warn("Zero actions found - check that you have some actions defined, with a view permission")
|
||||
}
|
||||
|
||||
log.Debugf("GetDashboardComponents: %v", res)
|
||||
log.Tracef("GetDashboardComponents: %v", res)
|
||||
|
||||
dashboardCfgToPb(res, cfg.Dashboards, cfg)
|
||||
|
||||
res.AuthenticatedUser = user.Username
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *pb.GetLogsRequest) (*pb.GetLogsResponse, error) {
|
||||
user := acl.UserFromContext(ctx, cfg)
|
||||
|
||||
ret := &pb.GetLogsResponse{}
|
||||
|
||||
// TODO Limit to 10 entries or something to prevent browser lag.
|
||||
|
||||
for _, logEntry := range api.executor.Logs {
|
||||
ret.Logs = append(ret.Logs, &pb.LogEntry{
|
||||
ActionTitle: logEntry.ActionTitle,
|
||||
ActionIcon: logEntry.ActionIcon,
|
||||
Datetime: logEntry.Datetime,
|
||||
Stdout: logEntry.Stdout,
|
||||
Stderr: logEntry.Stderr,
|
||||
TimedOut: logEntry.TimedOut,
|
||||
ExitCode: logEntry.ExitCode,
|
||||
Tags: logEntry.Tags,
|
||||
})
|
||||
for trackingId, logEntry := range api.executor.GetLogsCopy() {
|
||||
action := cfg.FindAction(logEntry.ActionTitle)
|
||||
|
||||
if action == nil || acl.IsAllowedLogs(cfg, user, action) {
|
||||
pbLogEntry := internalLogEntryToPb(logEntry)
|
||||
pbLogEntry.ExecutionTrackingId = trackingId
|
||||
|
||||
ret.Logs = append(ret.Logs, pbLogEntry)
|
||||
}
|
||||
}
|
||||
|
||||
sorter := func(i, j int) bool {
|
||||
return ret.Logs[i].DatetimeStarted < ret.Logs[j].DatetimeStarted
|
||||
}
|
||||
|
||||
sort.Slice(ret.Logs, sorter)
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -109,12 +316,67 @@ func (api *oliveTinAPI) WhoAmI(ctx ctx.Context, req *pb.WhoAmIRequest) (*pb.WhoA
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) SosReport(ctx ctx.Context, req *pb.SosReportRequest) (*pb.SosReportResponse, error) {
|
||||
res := &pb.SosReportResponse{
|
||||
Alert: "Your SOS Report has been logged to OliveTin logs.",
|
||||
func (api *oliveTinAPI) SosReport(ctx ctx.Context, req *pb.SosReportRequest) (*httpbody.HttpBody, error) {
|
||||
sos := installationinfo.GetSosReport()
|
||||
|
||||
if !cfg.InsecureAllowDumpSos {
|
||||
log.Info(sos)
|
||||
sos = "Your SOS Report has been logged to OliveTin logs.\n\nIf you are in a safe network, you can temporarily set `insecureAllowDumpSos: true` in your config.yaml, restart OliveTin, and refresh this page - it will put the output directly in the browser."
|
||||
}
|
||||
|
||||
log.Infof("\n" + installationinfo.GetSosReport())
|
||||
ret := &httpbody.HttpBody{
|
||||
ContentType: "text/plain",
|
||||
Data: []byte(sos),
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) DumpVars(ctx ctx.Context, req *pb.DumpVarsRequest) (*pb.DumpVarsResponse, error) {
|
||||
res := &pb.DumpVarsResponse{}
|
||||
|
||||
if !cfg.InsecureAllowDumpVars {
|
||||
res.Alert = "Dumping variables is not allowed by default because it is insecure."
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpVars = false again after you don't need it anymore"
|
||||
res.Contents = sv.GetAll()
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) DumpPublicIdActionMap(ctx ctx.Context, req *pb.DumpPublicIdActionMapRequest) (*pb.DumpPublicIdActionMapResponse, error) {
|
||||
res := &pb.DumpPublicIdActionMapResponse{}
|
||||
res.Contents = make(map[string]*pb.ActionEntityPair)
|
||||
|
||||
if !cfg.InsecureAllowDumpActionMap {
|
||||
res.Alert = "Dumping Public IDs is disallowed."
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
api.executor.MapActionIdToBindingLock.RLock()
|
||||
|
||||
for k, v := range api.executor.MapActionIdToBinding {
|
||||
res.Contents[k] = &pb.ActionEntityPair{
|
||||
ActionTitle: v.Action.Title,
|
||||
EntityPrefix: v.EntityPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
api.executor.MapActionIdToBindingLock.RUnlock()
|
||||
|
||||
res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpActionMap = false again after you don't need it anymore"
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) GetReadyz(ctx ctx.Context, req *pb.GetReadyzRequest) (*pb.GetReadyzResponse, error) {
|
||||
res := &pb.GetReadyzResponse{
|
||||
Status: "OK",
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
@@ -123,6 +385,10 @@ func (api *oliveTinAPI) SosReport(ctx ctx.Context, req *pb.SosReportRequest) (*p
|
||||
func Start(globalConfig *config.Config, ex *executor.Executor) {
|
||||
cfg = globalConfig
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"address": cfg.ListenAddressGrpcActions,
|
||||
}).Info("Starting gRPC API")
|
||||
|
||||
lis, err := net.Listen("tcp", cfg.ListenAddressGrpcActions)
|
||||
|
||||
if err != nil {
|
||||
@@ -130,7 +396,7 @@ func Start(globalConfig *config.Config, ex *executor.Executor) {
|
||||
}
|
||||
|
||||
grpcServer := grpc.NewServer()
|
||||
pb.RegisterOliveTinApiServer(grpcServer, newServer(ex))
|
||||
pb.RegisterOliveTinApiServiceServer(grpcServer, newServer(ex))
|
||||
|
||||
err = grpcServer.Serve(lis)
|
||||
|
||||
|
||||
@@ -1,34 +1,50 @@
|
||||
package grpcapi
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
pb "github.com/OliveTin/OliveTin/gen/grpc"
|
||||
acl "github.com/OliveTin/OliveTin/internal/acl"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
executor "github.com/OliveTin/OliveTin/internal/executor"
|
||||
sv "github.com/OliveTin/OliveTin/internal/stringvariables"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func actionsCfgToPb(cfgActions []config.Action, user *acl.AuthenticatedUser) *pb.GetDashboardComponentsResponse {
|
||||
func buildDashboardResponse(ex *executor.Executor, cfg *config.Config, user *acl.AuthenticatedUser) *pb.GetDashboardComponentsResponse {
|
||||
res := &pb.GetDashboardComponentsResponse{}
|
||||
|
||||
for _, action := range cfgActions {
|
||||
if !acl.IsAllowedView(cfg, user, &action) {
|
||||
ex.MapActionIdToBindingLock.RLock()
|
||||
|
||||
for actionId, actionBinding := range ex.MapActionIdToBinding {
|
||||
if !acl.IsAllowedView(cfg, user, actionBinding.Action) {
|
||||
continue
|
||||
}
|
||||
|
||||
btn := actionCfgToPb(action, user)
|
||||
res.Actions = append(res.Actions, btn)
|
||||
res.Actions = append(res.Actions, buildAction(actionId, actionBinding, user))
|
||||
}
|
||||
|
||||
ex.MapActionIdToBindingLock.RUnlock()
|
||||
|
||||
sort.Slice(res.Actions, func(i, j int) bool {
|
||||
if res.Actions[i].Order == res.Actions[j].Order {
|
||||
return res.Actions[i].Title < res.Actions[j].Title
|
||||
} else {
|
||||
return res.Actions[i].Order < res.Actions[j].Order
|
||||
}
|
||||
})
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func actionCfgToPb(action config.Action, user *acl.AuthenticatedUser) *pb.Action {
|
||||
func buildAction(actionId string, actionBinding *executor.ActionBinding, user *acl.AuthenticatedUser) *pb.Action {
|
||||
action := actionBinding.Action
|
||||
|
||||
btn := pb.Action{
|
||||
Id: fmt.Sprintf("%x", md5.Sum([]byte(action.Title))),
|
||||
Title: action.Title,
|
||||
Icon: action.Icon,
|
||||
CanExec: acl.IsAllowedExec(cfg, user, &action),
|
||||
Id: actionId,
|
||||
Title: sv.ReplaceEntityVars(actionBinding.EntityPrefix, action.Title),
|
||||
Icon: action.Icon,
|
||||
CanExec: acl.IsAllowedExec(cfg, user, action),
|
||||
PopupOnStart: action.PopupOnStart,
|
||||
Order: int32(actionBinding.ConfigOrder),
|
||||
}
|
||||
|
||||
for _, cfgArg := range action.Arguments {
|
||||
@@ -38,7 +54,8 @@ func actionCfgToPb(action config.Action, user *acl.AuthenticatedUser) *pb.Action
|
||||
Type: cfgArg.Type,
|
||||
Description: cfgArg.Description,
|
||||
DefaultValue: cfgArg.Default,
|
||||
Choices: buildChoices(cfgArg.Choices),
|
||||
Choices: buildChoices(cfgArg),
|
||||
Suggestions: cfgArg.Suggestions,
|
||||
}
|
||||
|
||||
btn.Arguments = append(btn.Arguments, &pbArg)
|
||||
@@ -47,7 +64,32 @@ func actionCfgToPb(action config.Action, user *acl.AuthenticatedUser) *pb.Action
|
||||
return &btn
|
||||
}
|
||||
|
||||
func buildChoices(choices []config.ActionArgumentChoice) []*pb.ActionArgumentChoice {
|
||||
func buildChoices(arg config.ActionArgument) []*pb.ActionArgumentChoice {
|
||||
if arg.Entity != "" && len(arg.Choices) == 1 {
|
||||
return buildChoicesEntity(arg.Choices[0], arg.Entity)
|
||||
} else {
|
||||
return buildChoicesSimple(arg.Choices)
|
||||
}
|
||||
}
|
||||
|
||||
func buildChoicesEntity(firstChoice config.ActionArgumentChoice, entityTitle string) []*pb.ActionArgumentChoice {
|
||||
ret := []*pb.ActionArgumentChoice{}
|
||||
|
||||
entityCount := sv.GetEntityCount(entityTitle)
|
||||
|
||||
for i := 0; i < entityCount; i++ {
|
||||
prefix := sv.GetEntityPrefix(entityTitle, i)
|
||||
|
||||
ret = append(ret, &pb.ActionArgumentChoice{
|
||||
Value: sv.ReplaceEntityVars(prefix, firstChoice.Value),
|
||||
Title: sv.ReplaceEntityVars(prefix, firstChoice.Title),
|
||||
})
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func buildChoicesSimple(choices []config.ActionArgumentChoice) []*pb.ActionArgumentChoice {
|
||||
ret := []*pb.ActionArgumentChoice{}
|
||||
|
||||
for _, cfgChoice := range choices {
|
||||
|
||||
67
internal/grpcapi/grpcApiDashboard.go
Normal file
67
internal/grpcapi/grpcApiDashboard.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package grpcapi
|
||||
|
||||
import (
|
||||
pb "github.com/OliveTin/OliveTin/gen/grpc"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func dashboardCfgToPb(res *pb.GetDashboardComponentsResponse, dashboards []*config.DashboardComponent, cfg *config.Config) {
|
||||
for _, dashboard := range dashboards {
|
||||
res.Dashboards = append(res.Dashboards, &pb.DashboardComponent{
|
||||
Type: "dashboard",
|
||||
Title: dashboard.Title,
|
||||
Contents: getDashboardComponentContents(dashboard, cfg),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getDashboardComponentContents(dashboard *config.DashboardComponent, cfg *config.Config) []*pb.DashboardComponent {
|
||||
ret := make([]*pb.DashboardComponent, 0)
|
||||
|
||||
for _, subitem := range dashboard.Contents {
|
||||
if subitem.Type == "fieldset" && subitem.Entity != "" {
|
||||
ret = append(ret, buildEntityFieldsets(subitem.Entity, &subitem)...)
|
||||
continue
|
||||
}
|
||||
|
||||
newitem := &pb.DashboardComponent{
|
||||
Title: subitem.Title,
|
||||
Type: getDashboardComponentType(&subitem),
|
||||
Contents: getDashboardComponentContents(&subitem, cfg),
|
||||
Icon: getDashboardComponentIcon(&subitem, cfg),
|
||||
CssClass: subitem.CssClass,
|
||||
}
|
||||
|
||||
ret = append(ret, newitem)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func getDashboardComponentIcon(item *config.DashboardComponent, cfg *config.Config) string {
|
||||
if item.Icon == "" {
|
||||
return cfg.DefaultIconForDirectories
|
||||
}
|
||||
|
||||
return item.Icon
|
||||
}
|
||||
|
||||
func getDashboardComponentType(item *config.DashboardComponent) string {
|
||||
allowedTypes := []string{
|
||||
"stdout-most-recent-execution",
|
||||
"display",
|
||||
}
|
||||
|
||||
if len(item.Contents) > 0 {
|
||||
if item.Type != "fieldset" {
|
||||
return "directory"
|
||||
}
|
||||
|
||||
return "fieldset"
|
||||
} else if slices.Contains(allowedTypes, item.Type) {
|
||||
return item.Type
|
||||
}
|
||||
|
||||
return "link"
|
||||
}
|
||||
51
internal/grpcapi/grpcApiDashboardEntities.go
Normal file
51
internal/grpcapi/grpcApiDashboardEntities.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package grpcapi
|
||||
|
||||
import (
|
||||
pb "github.com/OliveTin/OliveTin/gen/grpc"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
sv "github.com/OliveTin/OliveTin/internal/stringvariables"
|
||||
)
|
||||
|
||||
func buildEntityFieldsets(entityTitle string, tpl *config.DashboardComponent) []*pb.DashboardComponent {
|
||||
ret := make([]*pb.DashboardComponent, 0)
|
||||
|
||||
entityCount := sv.GetEntityCount(entityTitle)
|
||||
|
||||
for i := 0; i < entityCount; i++ {
|
||||
ret = append(ret, buildEntityFieldset(tpl, entityTitle, i))
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func buildEntityFieldset(tpl *config.DashboardComponent, entityTitle string, entityIndex int) *pb.DashboardComponent {
|
||||
prefix := sv.GetEntityPrefix(entityTitle, entityIndex)
|
||||
|
||||
return &pb.DashboardComponent{
|
||||
Title: sv.ReplaceEntityVars(prefix, tpl.Title),
|
||||
Type: "fieldset",
|
||||
Contents: buildEntityFieldsetContents(tpl.Contents, prefix),
|
||||
CssClass: sv.ReplaceEntityVars(prefix, tpl.CssClass),
|
||||
}
|
||||
}
|
||||
|
||||
func buildEntityFieldsetContents(contents []config.DashboardComponent, prefix string) []*pb.DashboardComponent {
|
||||
ret := make([]*pb.DashboardComponent, 0)
|
||||
|
||||
for _, subitem := range contents {
|
||||
clone := &pb.DashboardComponent{}
|
||||
clone.CssClass = sv.ReplaceEntityVars(prefix, subitem.CssClass)
|
||||
|
||||
if subitem.Type == "" || subitem.Type == "link" {
|
||||
clone.Type = "link"
|
||||
clone.Title = sv.ReplaceEntityVars(prefix, subitem.Title)
|
||||
} else {
|
||||
clone.Title = sv.ReplaceEntityVars(prefix, subitem.Title)
|
||||
clone.Type = subitem.Type
|
||||
}
|
||||
|
||||
ret = append(ret, clone)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
@@ -21,25 +21,27 @@ const bufSize = 1024 * 1024
|
||||
|
||||
var lis *bufconn.Listener
|
||||
|
||||
func init() {
|
||||
ex := executor.DefaultExecutor()
|
||||
func initServer(cfg *config.Config) *executor.Executor {
|
||||
ex := executor.DefaultExecutor(cfg)
|
||||
|
||||
lis = bufconn.Listen(bufSize)
|
||||
s := grpc.NewServer()
|
||||
pb.RegisterOliveTinApiServer(s, newServer(ex))
|
||||
pb.RegisterOliveTinApiServiceServer(s, newServer(ex))
|
||||
|
||||
go func() {
|
||||
if err := s.Serve(lis); err != nil {
|
||||
log.Fatalf("Server exited with error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return ex
|
||||
}
|
||||
|
||||
func bufDialer(context.Context, string) (net.Conn, error) {
|
||||
return lis.Dial()
|
||||
}
|
||||
|
||||
func getNewTestServerAndClient(t *testing.T, injectedConfig *config.Config) (*grpc.ClientConn, pb.OliveTinApiClient) {
|
||||
func getNewTestServerAndClient(t *testing.T, injectedConfig *config.Config) (*grpc.ClientConn, pb.OliveTinApiServiceClient) {
|
||||
cfg = injectedConfig
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -50,18 +52,24 @@ func getNewTestServerAndClient(t *testing.T, injectedConfig *config.Config) (*gr
|
||||
t.Fatalf("Failed to dial bufnet: %v", err)
|
||||
}
|
||||
|
||||
client := pb.NewOliveTinApiClient(conn)
|
||||
client := pb.NewOliveTinApiServiceClient(conn)
|
||||
|
||||
return conn, client
|
||||
}
|
||||
|
||||
func TestGetActionsAndStart(t *testing.T) {
|
||||
cfg = config.DefaultConfig()
|
||||
btn1 := config.Action{}
|
||||
|
||||
ex := initServer(cfg)
|
||||
|
||||
btn1 := &config.Action{}
|
||||
btn1.Title = "blat"
|
||||
btn1.ID = "blat"
|
||||
btn1.Shell = "echo 'test'"
|
||||
cfg.Actions = append(cfg.Actions, btn1)
|
||||
|
||||
ex.RebuildActionMap()
|
||||
|
||||
conn, client := getNewTestServerAndClient(t, cfg)
|
||||
|
||||
respGb, err := client.GetDashboardComponents(context.Background(), &pb.GetDashboardComponentsRequest{})
|
||||
@@ -76,7 +84,7 @@ func TestGetActionsAndStart(t *testing.T) {
|
||||
|
||||
log.Printf("Response: %+v", respGb)
|
||||
|
||||
respSa, err := client.StartAction(context.Background(), &pb.StartActionRequest{ActionName: "blat"})
|
||||
respSa, err := client.StartAction(context.Background(), &pb.StartActionRequest{ActionId: "blat"})
|
||||
|
||||
assert.Nil(t, err, "Empty err after start action")
|
||||
assert.NotNil(t, respSa, "Empty err after start action")
|
||||
|
||||
@@ -13,5 +13,9 @@ func StartServers(cfg *config.Config) {
|
||||
go StartSingleHTTPFrontend(cfg)
|
||||
}
|
||||
|
||||
if cfg.Prometheus.Enabled {
|
||||
go StartPrometheus(cfg)
|
||||
}
|
||||
|
||||
startRestAPIServer(cfg)
|
||||
}
|
||||
|
||||
19
internal/httpservers/prometheus.go
Normal file
19
internal/httpservers/prometheus.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package httpservers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/collectors"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
func StartPrometheus(cfg *config.Config) {
|
||||
if !cfg.Prometheus.DefaultGoMetrics {
|
||||
prometheus.Unregister(collectors.NewGoCollector())
|
||||
}
|
||||
|
||||
http.Handle("/", promhttp.Handler())
|
||||
http.ListenAndServe(cfg.ListenAddressPrometheus, nil)
|
||||
}
|
||||
@@ -23,6 +23,8 @@ func parseHttpHeaderForAuth(req *http.Request) (string, string) {
|
||||
username, ok := req.Header[cfg.AuthHttpHeaderUsername]
|
||||
|
||||
if !ok {
|
||||
log.Warnf("Config has AuthHttpHeaderUsername set to %v, but it was not found", cfg.AuthHttpHeaderUsername)
|
||||
|
||||
return "", ""
|
||||
}
|
||||
|
||||
@@ -30,10 +32,16 @@ func parseHttpHeaderForAuth(req *http.Request) (string, string) {
|
||||
usergroup, ok := req.Header[cfg.AuthHttpHeaderUserGroup]
|
||||
|
||||
if ok {
|
||||
log.Debugf("HTTP Header Auth found a username and usergroup")
|
||||
|
||||
return username[0], usergroup[0]
|
||||
} else {
|
||||
log.Warnf("Config has AuthHttpHeaderUserGroup set to %v, but it was not found", cfg.AuthHttpHeaderUserGroup)
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("HTTP Header Auth found a username, but usergroup is not being used")
|
||||
|
||||
return username[0], ""
|
||||
}
|
||||
|
||||
@@ -49,48 +57,55 @@ func parseRequestMetadata(ctx context.Context, req *http.Request) metadata.MD {
|
||||
username, usergroup = parseHttpHeaderForAuth(req)
|
||||
}
|
||||
|
||||
md := metadata.Pairs(
|
||||
"username", username,
|
||||
"usergroup", usergroup,
|
||||
)
|
||||
md := metadata.New(map[string]string {
|
||||
"username": username,
|
||||
"usergroup": usergroup,
|
||||
})
|
||||
|
||||
log.Debugf("jwt usable claims: %+v", md)
|
||||
log.Tracef("api request metadata: %+v", md)
|
||||
|
||||
return md
|
||||
}
|
||||
|
||||
func SetGlobalRestConfig(config *config.Config) {
|
||||
cfg = config
|
||||
}
|
||||
|
||||
func startRestAPIServer(globalConfig *config.Config) error {
|
||||
cfg = globalConfig
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"address": cfg.ListenAddressGrpcActions,
|
||||
"address": cfg.ListenAddressRestActions,
|
||||
}).Info("Starting REST API")
|
||||
|
||||
ctx := context.Background()
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
mux := newMux()
|
||||
|
||||
// The JSONPb.EmitDefaults is necssary, so "empty" fields are returned in JSON.
|
||||
return http.ListenAndServe(cfg.ListenAddressRestActions, cors.AllowCors(mux))
|
||||
}
|
||||
|
||||
func newMux() *runtime.ServeMux {
|
||||
// The MarshalOptions set some important compatibility settings for the webui. See below.
|
||||
mux := runtime.NewServeMux(
|
||||
runtime.WithMetadata(parseRequestMetadata),
|
||||
runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.HTTPBodyMarshaler{
|
||||
Marshaler: &runtime.JSONPb{
|
||||
MarshalOptions: protojson.MarshalOptions{
|
||||
UseProtoNames: true,
|
||||
EmitUnpopulated: true,
|
||||
UseProtoNames: false, // eg: canExec for js instead of can_exec from protobuf
|
||||
EmitUnpopulated: true, // Emit empty fields so that javascript does not get "undefined" when accessing fields with empty values.
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
opts := []grpc.DialOption{grpc.WithInsecure()}
|
||||
|
||||
err := gw.RegisterOliveTinApiHandlerFromEndpoint(ctx, mux, cfg.ListenAddressGrpcActions, opts)
|
||||
err := gw.RegisterOliveTinApiServiceHandlerFromEndpoint(ctx, mux, cfg.ListenAddressGrpcActions, opts)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Could not register REST API Handler %v", err)
|
||||
|
||||
return err
|
||||
log.Panicf("Could not register REST API Handler %v", err)
|
||||
}
|
||||
|
||||
return http.ListenAndServe(cfg.ListenAddressRestActions, cors.AllowCors(mux))
|
||||
return mux
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user