Compare commits
221 Commits
2024.10.02
...
280234b138
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
280234b138 | ||
|
|
02ec8eeb65 | ||
|
|
ef5a67e7b8 | ||
|
|
eb2463aa2d | ||
|
|
a7e7bf869e | ||
|
|
0dd9e9b2b7 | ||
|
|
aa8322c354 | ||
|
|
956e74a6b3 | ||
|
|
c9ff4d1a68 | ||
|
|
88cc1ab080 | ||
|
|
3b8bc49b04 | ||
|
|
31ea8507f5 | ||
|
|
62af851b2c | ||
|
|
2a764acde6 | ||
|
|
02e2ac1676 | ||
|
|
c89579840b | ||
|
|
38d81fafe2 | ||
|
|
8b2b85c3d0 | ||
|
|
76a33e2e54 | ||
|
|
fa94357374 | ||
|
|
439e952a25 | ||
|
|
3dfbbcc770 | ||
|
|
77e8c37599 | ||
|
|
d3aa3b25b0 | ||
|
|
d944b09c51 | ||
|
|
b9851adfde | ||
|
|
45f9c18bc3 | ||
|
|
5d947f5a32 | ||
|
|
754d216827 | ||
|
|
3d902295ad | ||
|
|
5f8cd60736 | ||
|
|
ae360100ce | ||
|
|
4a851355a8 | ||
|
|
54d3c65df3 | ||
|
|
58ba8eeeb9 | ||
|
|
e1e9cd9c35 | ||
|
|
2a5fe71458 | ||
|
|
cbb163726e | ||
|
|
6836062b00 | ||
|
|
339dbe6dbd | ||
|
|
a24a7fbd01 | ||
|
|
c9c781b197 | ||
|
|
b0f24811b2 | ||
|
|
3884dc6d0a | ||
|
|
91dfe2437e | ||
|
|
60814b97e2 | ||
|
|
b330fbd1a5 | ||
|
|
7d5fa999e5 | ||
|
|
a464e6a445 | ||
|
|
a26a8bb032 | ||
|
|
7345744e41 | ||
|
|
570c0ba087 | ||
|
|
60c0c5db27 | ||
|
|
4a847f0587 | ||
|
|
6b342cbedb | ||
|
|
f46a02fced | ||
|
|
3dd7aaff88 | ||
|
|
7d4edeb60a | ||
|
|
387f1d9c1a | ||
|
|
c526fa323e | ||
|
|
17c716c599 | ||
|
|
c5b49b33ab | ||
|
|
2a73b58255 | ||
|
|
a62d58f119 | ||
|
|
e02ce2be4e | ||
|
|
21ad5871ce | ||
|
|
d4d3193c1d | ||
|
|
10e5a92cbe | ||
|
|
a06299bd9e | ||
|
|
2d4a3fc048 | ||
|
|
81ef166d78 | ||
|
|
d4fe9eaa79 | ||
|
|
af75afa82f | ||
|
|
b91c0d7e45 | ||
|
|
e922baa2e0 | ||
|
|
fcd1879d09 | ||
|
|
a8ac719af7 | ||
|
|
b99b3f4345 | ||
|
|
b16ce074ea | ||
|
|
54447774d1 | ||
|
|
260477e5e8 | ||
|
|
e559c32c37 | ||
|
|
2c7b33b730 | ||
|
|
1970311ff5 | ||
|
|
433456986d | ||
|
|
e38361f3d7 | ||
|
|
1ffdd93ddf | ||
|
|
18c5599704 | ||
|
|
224d7f40ed | ||
|
|
1357eae9b8 | ||
|
|
b2e7509959 | ||
|
|
cebab32514 | ||
|
|
7110399d41 | ||
|
|
74f0930dcc | ||
|
|
c20eea29cd | ||
|
|
8c073bf45f | ||
|
|
fcfa007cec | ||
|
|
b83b7a4c42 | ||
|
|
0a7f3f3226 | ||
|
|
633e513697 | ||
|
|
eb2721c023 | ||
|
|
765c698a9b | ||
|
|
c19428f6b6 | ||
|
|
f02982b451 | ||
|
|
6ffb0cedbc | ||
|
|
775b3d3ca6 | ||
|
|
6ad001619d | ||
|
|
db28e8915b | ||
|
|
bb4969c9ac | ||
|
|
aef70c0e1b | ||
|
|
27c287c2de | ||
|
|
ec1f974f67 | ||
|
|
4ccfd0f993 | ||
|
|
c5eaa35fb0 | ||
|
|
24ba4fb574 | ||
|
|
b742bd89c4 | ||
|
|
2fcc0a63a0 | ||
|
|
ba29325c15 | ||
|
|
88f639d29f | ||
|
|
fa44e958d8 | ||
|
|
182548e0dc | ||
|
|
c3097e40db | ||
|
|
2b66414a93 | ||
|
|
7da8a5bc38 | ||
|
|
2bc45e9a09 | ||
|
|
fd3c6087f0 | ||
|
|
c0b8dd71db | ||
|
|
1164e5fae2 | ||
|
|
902d4ed819 | ||
|
|
2320a56dd9 | ||
|
|
2981fc4c1f | ||
|
|
8865331da2 | ||
|
|
709d6ac2ad | ||
|
|
8d4e335dda | ||
|
|
44a9de080c | ||
|
|
9bb17badad | ||
|
|
ff31abe66c | ||
|
|
b4c886d5d3 | ||
|
|
e0bc1d86f6 | ||
|
|
270f20ec75 | ||
|
|
486253b253 | ||
|
|
6b9c6c8b9c | ||
|
|
1275934ac1 | ||
|
|
bbc6095e36 | ||
|
|
7906f2d363 | ||
|
|
d45bd887c2 | ||
|
|
7788f58aac | ||
|
|
f3bc82311d | ||
|
|
cae5d296ca | ||
|
|
5cd5bd2a25 | ||
|
|
6550223ee4 | ||
|
|
39368d511a | ||
|
|
0a4bcc6423 | ||
|
|
56ab1cec8f | ||
|
|
12f87ca6e1 | ||
|
|
2fc7c23416 | ||
|
|
2cf538bab1 | ||
|
|
906b6c5783 | ||
|
|
6d43ebef44 | ||
|
|
c04203e671 | ||
|
|
bf93707787 | ||
|
|
f0d70f0c15 | ||
|
|
17d9e29f19 | ||
|
|
be7168d9c5 | ||
|
|
d36b23832c | ||
|
|
b6429e9bc7 | ||
|
|
7ddc112b2c | ||
|
|
10a473ca1c | ||
|
|
c7207d1ee6 | ||
|
|
c585762ba8 | ||
|
|
476838d59a | ||
|
|
f0b1cefb72 | ||
|
|
b4a555e3da | ||
|
|
79a4119351 | ||
|
|
851d0dac84 | ||
|
|
655a7f205d | ||
|
|
13e48dded2 | ||
|
|
344df3739d | ||
|
|
0d981773b3 | ||
|
|
8d4ddd36cd | ||
|
|
209234c09f | ||
|
|
9bcb2d80dc | ||
|
|
de504f5f80 | ||
|
|
8fe91101db | ||
|
|
8717997b0e | ||
|
|
1bfb7c4b28 | ||
|
|
964dc7b48a | ||
|
|
ceb0a78180 | ||
|
|
9117163316 | ||
|
|
d3ad811ac5 | ||
|
|
ee26fe6b50 | ||
|
|
2950b59514 | ||
|
|
be5ddda020 | ||
|
|
16b07283b0 | ||
|
|
d7814ff6df | ||
|
|
be9b2a7c78 | ||
|
|
80e5b5b0c1 | ||
|
|
0283b51eca | ||
|
|
1af2e92132 | ||
|
|
71ad5d2e3a | ||
|
|
32b5fee108 | ||
|
|
de81ec00fd | ||
|
|
7cc07158ab | ||
|
|
3b8976fd51 | ||
|
|
fb7e650267 | ||
|
|
6a7187fb5b | ||
|
|
b31cdf15a2 | ||
|
|
6e0e0e8133 | ||
|
|
706441799f | ||
|
|
0b66bc7bbd | ||
|
|
86e876a9c9 | ||
|
|
8b33061bbb | ||
|
|
411f1bff1c | ||
|
|
eee4c4e404 | ||
|
|
a187c8c8a0 | ||
|
|
35dca50863 | ||
|
|
00856f15a7 | ||
|
|
d1c67a9dd8 | ||
|
|
6c289506c2 | ||
|
|
5d82ae7680 | ||
|
|
9737886839 |
@@ -1,31 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import sys
|
||||
|
||||
commitmsg = ""
|
||||
|
||||
with open('.git/COMMIT_EDITMSG', mode='r') as f:
|
||||
commitmsg = f.readline().strip()
|
||||
|
||||
print("Commit message is: " + commitmsg)
|
||||
|
||||
ALLOWED_COMMIT_TYPES = [
|
||||
"cicd",
|
||||
"test",
|
||||
"refactor",
|
||||
"depbump",
|
||||
"typo",
|
||||
"fmt",
|
||||
"doc",
|
||||
"bugfix",
|
||||
"security",
|
||||
"feature",
|
||||
]
|
||||
|
||||
for allowedType in ALLOWED_COMMIT_TYPES:
|
||||
if commitmsg.startswith(allowedType + ":"):
|
||||
print("Allowing commit type: ", allowedType)
|
||||
sys.exit(0)
|
||||
|
||||
print("Commit message should start with commit type. One of: ", ", ".join(ALLOWED_COMMIT_TYPES))
|
||||
sys.exit(1)
|
||||
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -2,7 +2,9 @@
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ""
|
||||
labels: bug
|
||||
type: bug
|
||||
labels:
|
||||
- "waiting-on-developer"
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -2,7 +2,9 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
type: feature
|
||||
labels:
|
||||
- "waiting-on-developer"
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/support_request.md
vendored
@@ -2,7 +2,9 @@
|
||||
name: Support request
|
||||
about: Need some help? Got an error message?
|
||||
title: ""
|
||||
labels: support
|
||||
type: support
|
||||
labels:
|
||||
- "waiting-on-developer"
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
14
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -13,10 +13,14 @@ 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 read the [CONTRIBUTORS](CONTRIBUTORS.adoc) guide
|
||||
- [ ] I considered the "3 line" suggestion.
|
||||
- [ ] I followed the "1 logical change" rule.
|
||||
- [ ] 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 ran the `pre-commit` hooks, and my commit message was validated.
|
||||
- [ ] `make -wC service compile` runs without any issues.
|
||||
- [ ] `make -wC service codestyle` runs without any issues.
|
||||
- [ ] `make -wC service unittests` runs without any issues.
|
||||
- [ ] `make -wC webui codestyle` runs without any issues.
|
||||
- [ ] `make -w it` 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.
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
---
|
||||
name: "Build Tag"
|
||||
name: "Build & Release pipeline"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
branches:
|
||||
- main
|
||||
- next
|
||||
|
||||
jobs:
|
||||
build-tag:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -26,13 +31,14 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: webui.dev/package-lock.json
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
go-version-file: 'service/go.mod'
|
||||
cache: true
|
||||
cache-dependency-path: 'service/go.mod'
|
||||
|
||||
- name: Print go version
|
||||
run: go version
|
||||
@@ -50,31 +56,51 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.CONTAINER_TOKEN }}
|
||||
|
||||
- name: grpc
|
||||
run: make -w grpc
|
||||
- name: get date
|
||||
run: |
|
||||
echo "DATE=$(date +'%Y-%m-%d')" >> "$GITHUB_ENV"
|
||||
|
||||
- name: make webui
|
||||
run: make -w webui-dist
|
||||
|
||||
- name: goreleaser
|
||||
- name: unit tests
|
||||
run: make -w service-unittests
|
||||
|
||||
- name: build service
|
||||
run: make -w service
|
||||
|
||||
- name: integration tests
|
||||
run: cd integration-tests && make -w
|
||||
|
||||
- name: Archive integration tests
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
if: always()
|
||||
with:
|
||||
name: "OliveTin-integration-tests-${{ env.DATE }}-${{ github.sha }}"
|
||||
path: |
|
||||
integration-tests
|
||||
!integration-tests/node_modules
|
||||
|
||||
- name: Install goreleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --clean --parallelism 1
|
||||
install-only: true
|
||||
|
||||
- name: release
|
||||
if: github.ref_type != 'tag'
|
||||
uses: cycjimmy/semantic-release-action@v4
|
||||
with:
|
||||
extra_plugins: |
|
||||
@semantic-release/commit-analyzer
|
||||
@semantic-release/exec
|
||||
@semantic-release/git
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.CONTAINER_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.CONTAINER_TOKEN }}
|
||||
|
||||
- name: Archive binaries
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
with:
|
||||
name: "OliveTin-${{ github.ref_name }}"
|
||||
name: "OliveTin-snapshot-${{ env.DATE }}-${{ github.sha }}"
|
||||
path: dist/OliveTin*.*
|
||||
|
||||
- name: Archive integration tests
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
with:
|
||||
name: integration-tests
|
||||
path: |
|
||||
integration-tests
|
||||
!integration-tests/node_modules
|
||||
34
.github/workflows/build-buf.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Buf CI
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- 'proto/**'
|
||||
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled, unlabeled]
|
||||
delete:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
jobs:
|
||||
buf:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: 'service/go.mod'
|
||||
cache: true
|
||||
cache-dependency-path: 'service/go.mod'
|
||||
|
||||
- uses: bufbuild/buf-action@v1.1.0
|
||||
with:
|
||||
token: ${{ secrets.BUF_TOKEN }}
|
||||
# Change setup_only to true if you only want to set up the Action and not execute other commands.
|
||||
# Otherwise, you can delete this line--the default is false.
|
||||
setup_only: false
|
||||
# Optional GitHub token for API requests. Ensures requests aren't rate limited.
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
80
.github/workflows/build-snapshot.yml
vendored
@@ -1,80 +0,0 @@
|
||||
---
|
||||
name: "Build Snapshot"
|
||||
|
||||
on:
|
||||
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@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up QEMU
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
image: tonistiigi/binfmt:latest
|
||||
platforms: arm64,arm
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: webui.dev/package-lock.json
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
cache: true
|
||||
|
||||
- name: Print go version
|
||||
run: go version
|
||||
|
||||
- name: 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@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
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@v4.3.1
|
||||
with:
|
||||
name: "OliveTin-snapshot-${{ env.DATE }}-${{ github.sha }}"
|
||||
path: dist/OliveTin*.*
|
||||
|
||||
- name: Archive integration tests
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
with:
|
||||
name: "OliveTin-integration-tests-${{ env.DATE }}-${{ github.sha }}"
|
||||
path: |
|
||||
integration-tests
|
||||
!integration-tests/node_modules
|
||||
6
.github/workflows/codeql-analysis.yml
vendored
@@ -47,11 +47,9 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
go-version-file: 'service/go.mod'
|
||||
cache: true
|
||||
|
||||
- name: grpc
|
||||
run: make -w grpc
|
||||
cache-dependency-path: 'service/go.mod'
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
14
.github/workflows/codestyle.yml
vendored
@@ -21,17 +21,15 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
go-version-file: 'service/go.mod'
|
||||
cache: true
|
||||
cache-dependency-path: 'service/go.mod'
|
||||
|
||||
- name: Print go version
|
||||
run: go version
|
||||
|
||||
- name: deps
|
||||
run: make -w grpc
|
||||
- name: service
|
||||
run: make -wC service codestyle
|
||||
|
||||
- name: daemon
|
||||
run: make -w daemon-codestyle
|
||||
|
||||
- name: webui
|
||||
run: make -w webui-codestyle
|
||||
- name: frontend
|
||||
run: make -wC frontend codestyle
|
||||
|
||||
6
.github/workflows/devskim.yml
vendored
@@ -16,19 +16,19 @@ on:
|
||||
jobs:
|
||||
lint:
|
||||
name: DevSkim
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- 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
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: devskim-results.sarif
|
||||
|
||||
73
.github/workflows/issue-responsibility.yml
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
---
|
||||
name: Issue Responsibility
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
update-responsibility-labels:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Update responsibility labels
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const commentAuthor = context.payload.comment.user.login;
|
||||
const issueNumber = context.payload.issue.number;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
const skipAction = context.payload.comment.body.includes("/skip-responsibility");
|
||||
|
||||
if (skipAction) {
|
||||
core.info("Skipping responsibility label update");
|
||||
return;
|
||||
}
|
||||
|
||||
const developers = ["jamesread"]
|
||||
const commenterIsDeveloper = developers.includes(commentAuthor);
|
||||
const commenterIsUser = !commenterIsDeveloper;
|
||||
|
||||
const issueLabels = context.payload.issue.labels.map(label => label.name);
|
||||
|
||||
if (issueLabels.includes("waiting-on-developer")) {
|
||||
if (commenterIsDeveloper) {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
name: "waiting-on-developer",
|
||||
});
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
labels: ["waiting-on-requestor"],
|
||||
});
|
||||
|
||||
core.info(`Switched responsibility to user for issue #${issueNumber}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (issueLabels.includes("waiting-on-requestor")) {
|
||||
if (commenterIsUser) {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
name: "waiting-on-requestor",
|
||||
});
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
labels: ["waiting-on-developer"],
|
||||
});
|
||||
|
||||
core.info(`Switched responsibility to developer for issue #${issueNumber}`);
|
||||
}
|
||||
}
|
||||
19
.gitignore
vendored
@@ -1,15 +1,18 @@
|
||||
**/*.swp
|
||||
**/*.swo
|
||||
gen/
|
||||
/OliveTin
|
||||
/OliveTin.armhf
|
||||
/OliveTin.exe
|
||||
reports
|
||||
service/OliveTin
|
||||
service/OliveTin.armhf
|
||||
service/OliveTin.exe
|
||||
service/reports
|
||||
releases/
|
||||
dist/
|
||||
installation-id.txt
|
||||
tmp/
|
||||
frontend/dist/
|
||||
frontend/node_modules
|
||||
custom-frontend
|
||||
integration-tests/screenshots/
|
||||
.vscode/
|
||||
webui/
|
||||
webui.dev/node_modules
|
||||
webui.dev/.parcel-cache
|
||||
custom-webui
|
||||
server.log
|
||||
OliveTin
|
||||
|
||||
@@ -2,28 +2,31 @@ project_name: OliveTin
|
||||
version: 2
|
||||
before:
|
||||
hooks:
|
||||
- go mod download
|
||||
- make service-prep
|
||||
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
binary: OliveTin
|
||||
main: main.go
|
||||
dir: service
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
- freebsd
|
||||
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
- arm
|
||||
- riscv64
|
||||
|
||||
goarm:
|
||||
- 5 # For old RPIs
|
||||
- 6
|
||||
- 7
|
||||
|
||||
main: cmd/OliveTin/main.go
|
||||
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: arm # Mac does not work on [32bit] arm
|
||||
@@ -40,7 +43,7 @@ builds:
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ .Branch }}-{{ .ShortCommit }}"
|
||||
version_template: "{{ .Branch }}-{{ .ShortCommit }}"
|
||||
changelog:
|
||||
sort: asc
|
||||
groups:
|
||||
@@ -48,7 +51,7 @@ changelog:
|
||||
regexp: '^.*?security(\([[:word:]]+\))??!?:.+$'
|
||||
order: 0
|
||||
- title: 'Features'
|
||||
regexp: '^.*?feature(\([[:word:]]+\))??!?:.+$'
|
||||
regexp: '^.*?feat.*?(\([[:word:]]+\))??!?:.+$'
|
||||
order: 1
|
||||
- title: 'Bug fixes'
|
||||
regexp: '^.*?bugfix(\([[:word:]]+\))??!?:.+$'
|
||||
@@ -63,25 +66,19 @@ changelog:
|
||||
- '^refactor:'
|
||||
|
||||
archives:
|
||||
-
|
||||
format: tar.gz
|
||||
|
||||
- formats: tar.gz
|
||||
files:
|
||||
- config.yaml
|
||||
- LICENSE
|
||||
- README.md
|
||||
- Dockerfile
|
||||
- webui
|
||||
- OliveTin.service
|
||||
- ./var/
|
||||
|
||||
name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}{{ .Arm }}"
|
||||
|
||||
wrap_in_directory: true
|
||||
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
formats: zip
|
||||
|
||||
dockers:
|
||||
- image_templates:
|
||||
@@ -129,6 +126,12 @@ docker_manifests:
|
||||
- docker.io/jamesread/olivetin:{{ .Version }}-amd64
|
||||
- docker.io/jamesread/olivetin:{{ .Version }}-arm64
|
||||
|
||||
- name_template: docker.io/jamesread/olivetin:latest-3k
|
||||
image_templates:
|
||||
- docker.io/jamesread/olivetin:{{ .Version }}-amd64
|
||||
- docker.io/jamesread/olivetin:{{ .Version }}-arm64
|
||||
|
||||
|
||||
- name_template: ghcr.io/olivetin/olivetin:{{ .Version }}
|
||||
image_templates:
|
||||
- ghcr.io/olivetin/olivetin:{{ .Version }}-amd64
|
||||
@@ -139,6 +142,12 @@ docker_manifests:
|
||||
- ghcr.io/olivetin/olivetin:{{ .Version }}-amd64
|
||||
- ghcr.io/olivetin/olivetin:{{ .Version }}-arm64
|
||||
|
||||
- name_template: ghcr.io/olivetin/olivetin:latest-3k
|
||||
image_templates:
|
||||
- ghcr.io/olivetin/olivetin:{{ .Version }}-amd64
|
||||
- ghcr.io/olivetin/olivetin:{{ .Version }}-arm64
|
||||
|
||||
|
||||
nfpms:
|
||||
- id: default
|
||||
maintainer: James Read <contact@jread.com>
|
||||
@@ -154,7 +163,7 @@ nfpms:
|
||||
file_name_template: '{{ .PackageName }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||
|
||||
contents:
|
||||
- src: OliveTin.service
|
||||
- src: var/systemd/OliveTin.service
|
||||
dst: /etc/systemd/system/OliveTin.service
|
||||
|
||||
- src: webui/*
|
||||
@@ -217,7 +226,7 @@ release:
|
||||
|
||||
## Useful links
|
||||
|
||||
- [Which download do I need?](https://docs.olivetin.app/choose-package.html)
|
||||
- [Which download do I need?](https://docs.olivetin.app/install/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!
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
---
|
||||
# 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
|
||||
- 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
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
|
||||
# Alternative semantic commit checker
|
||||
- repo: https://github.com/compilerla/conventional-pre-commit
|
||||
rev: v3.1.0
|
||||
hooks:
|
||||
- id: conventional-pre-commit
|
||||
stages: [commit-msg]
|
||||
args: [] # optional: list of Conventional Commits types to allow e.g. [feat, fix, ci, chore, test]
|
||||
|
||||
16
.releaserc.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
branches:
|
||||
- name: main
|
||||
# range: '3000.x.x'
|
||||
|
||||
# - name: release/2k
|
||||
# range: '>=2000.0.0 <3000.0.0'
|
||||
|
||||
plugins:
|
||||
- '@semantic-release/commit-analyzer'
|
||||
- '@semantic-release/git'
|
||||
- - "@semantic-release/exec"
|
||||
- publishCmd: |
|
||||
goreleaser release --clean --timeout 60m
|
||||
|
||||
tagFormat: '${version}'
|
||||
69
AGENTS.md
Normal file
@@ -0,0 +1,69 @@
|
||||
## OliveTin – Agent Guide
|
||||
|
||||
This document helps AI agents contribute effectively to OliveTin.
|
||||
|
||||
If you are looking for OliveTin's AI policy, you can find it in `AI.md`.
|
||||
|
||||
### Project Overview
|
||||
- **Service (Go)**: `service/` with business logic under `service/internal/*`
|
||||
- API (Connect RPC): `service/internal/api`
|
||||
- Command execution: `service/internal/executor`
|
||||
- HTTP frontends/proxy: `service/internal/httpservers`
|
||||
- Config/types/entities: `service/internal/config`, `service/internal/entities`
|
||||
- **Frontend (Vue 3)**: `frontend/` (served by the service)
|
||||
- **Integration tests**: `integration-tests/`
|
||||
- **Protos/Generated**: `proto/`, `service/gen/...`
|
||||
|
||||
### How to Run
|
||||
- Run the server (dev):
|
||||
- From repo root: `go run ./service`
|
||||
- Unit tests (Go):
|
||||
- From repo root: `cd service && make unittests`
|
||||
- Integration tests (Mocha + Selenium):
|
||||
- Single test: `cd integration-tests && npx --yes mocha test/general.mjs`
|
||||
- All tests: `cd integration-tests && npx --yes mocha`
|
||||
|
||||
### Test Notes and Gotchas
|
||||
- The top-level Makefile does not expose `unittests`; use `cd service && make unittests`.
|
||||
- Connect RPC API must be mounted correctly; in tests, create the handler via `GetNewHandler(ex)` and serve under `/api/`.
|
||||
- Frontend “ready” state: the app sets `document.body` attribute `initial-marshal-complete="true"` when loaded. Integration helpers wait for this before selecting elements.
|
||||
- Modern UI uses Vue components:
|
||||
- Action buttons are rendered as `.action-button button`.
|
||||
- Logs and Diagnostics are Vue router links available via `/logs` and `/diagnostics`.
|
||||
- Some legacy DOM ids (e.g., `contentActions`) no longer exist; prefer class-based selectors.
|
||||
- Hidden UI features:
|
||||
- Footer visibility is controlled by `showFooter` from Init API; tests may assert the footer is absent when config disables it.
|
||||
|
||||
### Coding Standards (Go)
|
||||
- Avoid adding superflous comments that explain what the code is doing. Comments are only to describe business logic decisions.
|
||||
- Prefer clear, descriptive names; avoid 1–2 letter identifiers.
|
||||
- Use early returns and handle edge cases first.
|
||||
- Do not swallow errors; propagate or log meaningfully.
|
||||
- Match existing formatting; avoid unrelated reformatting.
|
||||
- Be safe around nils in executor steps (e.g., guard `req.Binding` and `req.Binding.Action`).
|
||||
|
||||
### API and Execution Flow (High-level)
|
||||
1. Client calls Connect RPC (e.g., `Init`, `GetDashboard`, `StartAction`).
|
||||
2. API translates requests to `executor.ExecutionRequest` and calls `Executor.ExecRequest`.
|
||||
3. Executor runs a chain of steps: request binding → concurrency/rate/ACL checks → arg parsing → exec → post-exec → logging/triggering.
|
||||
4. Logs are stored and can be fetched via `ExecutionStatus`/`GetLogs`.
|
||||
|
||||
### Common Tasks
|
||||
- Add/modify actions: update `config.yaml` and ensure `executor.RebuildActionMap()` is called when needed.
|
||||
- Adjust dashboard rendering: see `service/internal/api/dashboards.go` and `apiActions.go`.
|
||||
- Frontend behavior:
|
||||
- Router: `frontend/resources/vue/router.js`
|
||||
- Main shell/layout: `frontend/resources/vue/App.vue`
|
||||
- Action button behavior: `frontend/resources/vue/ActionButton.vue`
|
||||
|
||||
### Contributing Checklist
|
||||
- Review the contributing guidelines at `CONTRIBUTING.adoc`.
|
||||
- Review the AI guidance in `AI.md`.
|
||||
- Review the pull request template at `.github/PULL_REQUEST_TEMPLATE.md`.
|
||||
|
||||
### Troubleshooting
|
||||
- API tests failing with content-type errors: ensure Connect handler is served under `/api/` and the client targets that base URL.
|
||||
- Executor panics: check for nil `Binding/Action` and add guards in step functions.
|
||||
- Integration timeouts: wait for `initial-marshal-complete` and use selectors matching the Vue UI.
|
||||
|
||||
|
||||
32
AI.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# OliveTin's AI Policy
|
||||
|
||||
## Runtime:
|
||||
|
||||
- [x] The project does not include any AI functionality at runtime.
|
||||
- [x] No data, usage, or similar is sent to, or analyized by AI.
|
||||
|
||||
## Development - Contributions
|
||||
|
||||
- [x] The project **does accept** contributions that were written with AI help. **However**:
|
||||
- The contribution must be attributed to a human username who takes responsibility for the code as if they wrote it themselves.
|
||||
- AI often generates very unmaintainable code as it gets longer - loads of duplication, very little function re-use amd very poor at following style guides / idiomatic design. All code contributions (AI or not) are scrutinized hard for **maintainability** and **clean merging**. Please follow the CONTRIBUTORS guide.
|
||||
- AI that helps with short tab completion is generally fine.
|
||||
- AI that writes lots of new code across lots of files, or makes lots of superfluous changes is generally less likely to be accepted.
|
||||
- Vibe coding is not a suitable way to contribute to this project.
|
||||
- [x] Contributors should declare when AI has been used to help write contributions in the pull request body message.
|
||||
- [x] The project uses AI as an **optional** part of the PR process (coderabbitai). Please raise any concerns about usage within the PR.
|
||||
- [x] Suggestions from coderabbitai can be accepted verbaitem, but ideally it should be the PR author that uses coderabbitai as a guide, who then re-writes the contribution.
|
||||
- [x] Maintainers are the only agents permitted to accept merges.
|
||||
|
||||
## Development - Build process
|
||||
|
||||
- [x] Linters, code review tools, and others which are enabled by AI are allowed, but cannot be added as part of the standard build process.
|
||||
|
||||
## Community
|
||||
|
||||
- [x] Only project admins are allowed to run bots in the community discord server (at the time of writing, the current bot, Japella, is not AI-enabled).
|
||||
- [x] Support is currently not provided by AI.
|
||||
|
||||
## Training
|
||||
|
||||
- [x] You may use the OliveTin code base, documentation and repositories to train AI, but obviously usernames and personally identifiable information may not be stored.
|
||||
@@ -11,6 +11,14 @@ Ideas may be discussed, purely on their merits and issues. Our Code of Conduct
|
||||
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].
|
||||
|
||||
== Suggestion: More than 3 lines - talk to someone first
|
||||
|
||||
If you're planning on making a change that's more than a 3 lines, please talk to someone first (raising a GitHub issue is the best way to do that). This is so that you don't waste your time on something that might not be accepted. It's also a good way to get some feedback on your idea and make sure you're on the right track.
|
||||
|
||||
== Rule: A PR should be one logical change
|
||||
|
||||
Please try to keep your pull requests small and focused. It's almost impossible to review PRs that change lots of files for lots of different reasons. If you have a big change, it's probably best to break it down into smaller, more manageable chunks, otherwise it's likely to be rejected.
|
||||
|
||||
== If you're not sure, ask!
|
||||
|
||||
Don't be afraid to ask for advice before working on a
|
||||
@@ -22,7 +30,7 @@ the general direction and roadmap of this project without asking.
|
||||
|
||||
The preferred way to communicate is probably via Discord or GitHub issues.
|
||||
|
||||
=== Dev environment setup and clean build
|
||||
== Dev environment setup and clean build
|
||||
|
||||
```
|
||||
# Step1: setup compile env
|
||||
@@ -37,10 +45,10 @@ cd OliveTin
|
||||
make githooks
|
||||
|
||||
# Step3: compile binary for current dev env (OS, ARCH)
|
||||
# `make grpc` will also run `make go-tools`, which installs "buf". This binary
|
||||
# `make proto` 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
|
||||
# generate the protobuf / Connect RPC stubs.
|
||||
make proto
|
||||
make
|
||||
./OliveTin
|
||||
```
|
||||
@@ -50,7 +58,7 @@ make
|
||||
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+Connect RPC - you will need to `make proto`.
|
||||
* 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.
|
||||
|
||||
|
||||
20
Dockerfile
@@ -1,20 +1,34 @@
|
||||
FROM --platform=linux/amd64 registry.fedoraproject.org/fedora-minimal:40-x86_64
|
||||
FROM --platform=linux/amd64 registry.fedoraproject.org/fedora-minimal:42-x86_64 AS olivetin-tmputils
|
||||
|
||||
RUN microdnf -y install dnf-plugins-core && \
|
||||
dnf-3 config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo && \
|
||||
microdnf install -y docker-ce-cli docker-compose-plugin && microdnf clean all
|
||||
|
||||
FROM --platform=linux/amd64 registry.fedoraproject.org/fedora-minimal:42-x86_64
|
||||
|
||||
LABEL org.opencontainers.image.source https://github.com/OliveTin/OliveTin
|
||||
LABEL org.opencontainers.image.title=OliveTin
|
||||
LABEL org.opencontainers.image.title OliveTin
|
||||
|
||||
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 \
|
||||
kubernetes-client \
|
||||
shadow-utils \
|
||||
apprise \
|
||||
jq \
|
||||
git \
|
||||
docker \
|
||||
&& microdnf clean all
|
||||
|
||||
COPY --from=olivetin-tmputils \
|
||||
/usr/bin/docker \
|
||||
/usr/bin/docker
|
||||
|
||||
COPY --from=olivetin-tmputils \
|
||||
/usr/libexec/docker/cli-plugins/docker-compose \
|
||||
/usr/libexec/docker/cli-plugins/docker-compose
|
||||
|
||||
RUN useradd --system --create-home olivetin -u 1000
|
||||
|
||||
EXPOSE 1337/tcp
|
||||
|
||||
@@ -1,20 +1,34 @@
|
||||
FROM --platform=linux/arm64 registry.fedoraproject.org/fedora-minimal:40-aarch64
|
||||
FROM --platform=linux/arm64 registry.fedoraproject.org/fedora-minimal:42-aarch64 AS olivetin-tmputils
|
||||
|
||||
RUN microdnf -y install dnf-plugins-core && \
|
||||
dnf-3 config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo && \
|
||||
microdnf install -y docker-ce-cli docker-compose-plugin && microdnf clean all
|
||||
|
||||
FROM --platform=linux/arm64 registry.fedoraproject.org/fedora-minimal:42-aarch64
|
||||
|
||||
LABEL org.opencontainers.image.source https://github.com/OliveTin/OliveTin
|
||||
LABEL org.opencontainers.image.title=OliveTin
|
||||
LABEL org.opencontainers.image.title 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 \
|
||||
kubernetes-client \
|
||||
shadow-utils \
|
||||
apprise \
|
||||
jq \
|
||||
git \
|
||||
docker \
|
||||
&& microdnf clean all
|
||||
|
||||
COPY --from=olivetin-tmputils \
|
||||
/usr/bin/docker \
|
||||
/usr/bin/docker
|
||||
|
||||
COPY --from=olivetin-tmputils \
|
||||
/usr/libexec/docker/cli-plugins/docker-compose \
|
||||
/usr/libexec/docker/cli-plugins/docker-compose
|
||||
|
||||
RUN useradd --system --create-home olivetin -u 1000
|
||||
|
||||
EXPOSE 1337/tcp
|
||||
|
||||
50
Jenkinsfile
vendored
@@ -1,50 +0,0 @@
|
||||
pipeline {
|
||||
agent any
|
||||
|
||||
options {
|
||||
skipDefaultCheckout(true)
|
||||
}
|
||||
|
||||
stages {
|
||||
stage ('Pre-Build') {
|
||||
steps {
|
||||
cleanWs()
|
||||
checkout scm
|
||||
|
||||
sh 'make go-tools'
|
||||
}
|
||||
}
|
||||
|
||||
stage('Compile') {
|
||||
steps {
|
||||
withEnv(["PATH+GO=/root/go/bin/"]) {
|
||||
sh 'go env'
|
||||
sh 'echo $PATH'
|
||||
sh 'buf generate'
|
||||
sh 'make daemon-compile'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage ('Post-Compile') {
|
||||
parallel {
|
||||
stage('Codestyle') {
|
||||
steps {
|
||||
withEnv(["PATH+GO=/root/go/bin/"]) {
|
||||
sh 'make daemon-codestyle'
|
||||
sh 'make webui-codestyle'
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('UnitTests') {
|
||||
steps {
|
||||
withEnv(["PATH+GO=/root/go/bin/"]) {
|
||||
sh 'make daemon-unittests'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
75
Makefile
@@ -2,63 +2,27 @@ define delete-files
|
||||
python -c "import shutil;shutil.rmtree('$(1)', ignore_errors=True)"
|
||||
endef
|
||||
|
||||
compile: daemon-compile-currentenv
|
||||
service:
|
||||
$(MAKE) -wC service
|
||||
|
||||
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:
|
||||
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
|
||||
gocritic check ./...
|
||||
|
||||
daemon-unittests:
|
||||
$(call delete-files,reports)
|
||||
mkdir reports
|
||||
go test ./... -coverprofile reports/unittests.out
|
||||
go tool cover -html=reports/unittests.out -o reports/unittests.html
|
||||
service-prep:
|
||||
$(MAKE) -wC service prep
|
||||
|
||||
service-unittests:
|
||||
$(MAKE) -wC service unittests
|
||||
|
||||
it:
|
||||
cd integration-tests && make
|
||||
|
||||
githooks:
|
||||
git config --local core.hooksPath .githooks
|
||||
$(MAKE) -wC integration-tests
|
||||
|
||||
go-tools:
|
||||
go install "github.com/bufbuild/buf/cmd/buf"
|
||||
go install "github.com/fzipp/gocyclo/cmd/gocyclo"
|
||||
go install "github.com/go-critic/go-critic/cmd/gocritic"
|
||||
go install "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway"
|
||||
go install "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2"
|
||||
go install "google.golang.org/grpc/cmd/protoc-gen-go-grpc"
|
||||
go install "google.golang.org/protobuf/cmd/protoc-gen-go"
|
||||
$(MAKE) -wC service go-tools
|
||||
|
||||
grpc: go-tools
|
||||
buf generate
|
||||
proto: go-tools
|
||||
$(MAKE) -wC proto
|
||||
|
||||
dist: protoc
|
||||
dist:
|
||||
echo "dist noop"
|
||||
|
||||
protoc:
|
||||
protoc --go_out=. --go-grpc_out=. --grpc-gateway_out=. -I .:/usr/include/ OliveTin.proto
|
||||
|
||||
podman-image:
|
||||
buildah bud -t olivetin
|
||||
@@ -80,18 +44,9 @@ devrun: compile
|
||||
|
||||
devcontainer: compile podman-image podman-container
|
||||
|
||||
webui-codestyle:
|
||||
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')]"
|
||||
$(MAKE) -wC frontend dist
|
||||
mv frontend/dist webui
|
||||
|
||||
clean:
|
||||
$(call delete-files,dist)
|
||||
@@ -101,4 +56,4 @@ clean:
|
||||
$(call delete-files,reports)
|
||||
$(call delete-files,gen)
|
||||
|
||||
.PHONY: grpc
|
||||
.PHONY: proto service
|
||||
|
||||
301
OliveTin.proto
@@ -1,301 +0,0 @@
|
||||
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 can_exec = 4;
|
||||
repeated ActionArgument arguments = 5;
|
||||
string popup_on_start = 6;
|
||||
int32 order = 7;
|
||||
}
|
||||
|
||||
message ActionArgument {
|
||||
string name = 1;
|
||||
string title = 2;
|
||||
string type = 3;
|
||||
string default_value = 4;
|
||||
|
||||
repeated ActionArgumentChoice choices = 5;
|
||||
|
||||
string description = 6;
|
||||
map<string, string> suggestions = 7;
|
||||
}
|
||||
|
||||
message ActionArgumentChoice {
|
||||
string value = 1;
|
||||
string title = 2;
|
||||
}
|
||||
|
||||
message Entity {
|
||||
string title = 1;
|
||||
string icon = 2;
|
||||
repeated Action actions = 3;
|
||||
}
|
||||
|
||||
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 action_id = 1;
|
||||
|
||||
repeated StartActionArgument arguments = 2;
|
||||
|
||||
string unique_tracking_id = 3;
|
||||
}
|
||||
|
||||
message StartActionArgument {
|
||||
string name = 1;
|
||||
string value = 2;
|
||||
}
|
||||
|
||||
message StartActionResponse {
|
||||
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_started = 1;
|
||||
string action_title = 2;
|
||||
string output = 3;
|
||||
bool timed_out = 5;
|
||||
int32 exit_code = 6;
|
||||
string user = 7;
|
||||
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 {
|
||||
repeated LogEntry logs = 1;
|
||||
}
|
||||
|
||||
message ValidateArgumentTypeRequest {
|
||||
string value = 1;
|
||||
string type = 2;
|
||||
}
|
||||
|
||||
message ValidateArgumentTypeResponse {
|
||||
bool valid = 1;
|
||||
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 authenticated_user = 1;
|
||||
}
|
||||
|
||||
message SosReportRequest {}
|
||||
|
||||
message SosReportResponse {
|
||||
string alert = 1;
|
||||
}
|
||||
|
||||
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"
|
||||
};
|
||||
}
|
||||
|
||||
rpc StartAction(StartActionRequest) returns (StartActionResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/StartAction"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
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"
|
||||
};
|
||||
}
|
||||
|
||||
rpc ValidateArgumentType(ValidateArgumentTypeRequest) returns (ValidateArgumentTypeResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/ValidateArgumentType"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
rpc WhoAmI(WhoAmIRequest) returns (WhoAmIResponse) {
|
||||
option (google.api.http) = {
|
||||
get: "/api/WhoAmI"
|
||||
};
|
||||
}
|
||||
|
||||
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"
|
||||
};
|
||||
}
|
||||
}
|
||||
91
README.md
@@ -1,19 +1,24 @@
|
||||
# OliveTin
|
||||
<div align = "center">
|
||||
<img alt = "project logo" src = "https://github.com/OliveTin/OliveTin/blob/main/frontend/OliveTinLogo.png" width = "128" />
|
||||
<h1>OliveTin</h1>
|
||||
|
||||
<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.
|
||||
|
||||
[](#none)
|
||||
[](https://discord.gg/jhYWWpNJ3v)
|
||||
[](https://github.com/awesome-selfhosted/awesome-selfhosted#automation)
|
||||
[](https://bestpractices.coreinfrastructure.org/projects/5050)
|
||||
|
||||
[](https://goreportcard.com/report/github.com/OliveTin/OliveTin)
|
||||
[](https://github.com/OliveTin/OliveTin/actions/workflows/build-snapshot.yml)
|
||||
|
||||
<img alt = "screenshot" src = "https://github.com/OliveTin/OliveTin/blob/main/var/marketing/screenshotDesktop.png" />
|
||||
[OliveTin 2k to 3k upgrade guide](https://docs.olivetin.app/upgrade/2k3k.html)
|
||||
</div>
|
||||
|
||||
<img alt = "screenshot" src = "https://github.com/OliveTin/OliveTin/blob/main/var/marketing/screenshots/mainpage-laptop_framed.png" />
|
||||
<a href = "#screenshots">More screenshots below</a>
|
||||
|
||||
All documentation can be found at [docs.olivetin.app](https://docs.olivetin.app). This includes installation and usage guide, etc.
|
||||
|
||||
## Use cases
|
||||
|
||||
**Safely** give access to commands, for less technical people;
|
||||
@@ -41,8 +46,8 @@ OliveTin gives **safe** and **simple** access to predefined shell commands from
|
||||
* **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.
|
||||
* **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.
|
||||
* **Integrate with anything** - OliveTin just runs Linux shell commands, so theoretically 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/Connect RPC API.
|
||||
* **Good amount of unit tests and style checks** - helps potential contributors be consistent, and helps with maintainability.
|
||||
|
||||
## Screenshots
|
||||
@@ -50,73 +55,33 @@ 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" />
|
||||
<img alt = "screenshot" src = "https://github.com/OliveTin/OliveTin/blob/main/var/marketing/screenshots/mainpage-laptop_framed.png" />
|
||||
</p>
|
||||
|
||||
Desktop web browser (dark mode);
|
||||
|
||||
<p align = "center">
|
||||
<img alt = "screenshot" src = "https://github.com/OliveTin/OliveTin/blob/main/var/marketing/screenshotDesktopDark.png" />
|
||||
<img alt = "screenshot" src = "https://github.com/OliveTin/OliveTin/blob/main/var/marketing/screenshots/mainpage-darkop_framed.png" />
|
||||
</p>
|
||||
|
||||
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;" />
|
||||
<img alt = "screenshot" src = "https://github.com/OliveTin/OliveTin/blob/main/var/marketing/screenshots/mainpage-phone_framed.png" style = "width: 700px;" />
|
||||
</p>
|
||||
|
||||
## No-Nonsense Software Principles
|
||||
|
||||
OliveTin follows these principles:
|
||||
|
||||
* **Open Source & Free Software**: following the [Open Source Definition](https://opensource.org/osd) and the [Free Software Definition](https://www.gnu.org/philosophy/free-sw.html). All code and assets are available under the [AGPL-3.0 License](LICENSE).
|
||||
* **Independent**: No company owns the code or is responsible for the projects' governance.
|
||||
* **Inclusive**: No "core", "pro", "premium" or "enterprise" version. The only version is the one you can download and run, and it has all the features.
|
||||
* **Invisible**: No usage tracking, no user tracking, no ads, and no telemetry.
|
||||
* **Internal**: No internet connection required for any functionality.
|
||||
|
||||
## Documentation
|
||||
|
||||
All documentation can be found at http://docs.olivetin.app . This includes installation and usage guide, etc.
|
||||
All documentation can be found at [docs.olivetin.app](https://docs.olivetin.app). This includes installation and usage guide, etc.
|
||||
|
||||
### Quickstart reference for `config.yaml`
|
||||
|
||||
This is a quick example of `config.yaml` - but again, lots of documentation for how to write your `config.yaml` can be found at [the documentation site.](https://docs.olivetin.app)
|
||||
|
||||
* (Recommended) [Linux package install (.rpm/.deb)](https://docs.olivetin.app/install-linuxpackage.html) install instructions
|
||||
* [Container (podman/docker)](https://docs.olivetin.app/install-container.html) install instructions
|
||||
* [Docker compose](https://docs.olivetin.app/install-compose.html) install instructions
|
||||
* [Helm on Kubernetes](https://docs.olivetin.app/install-helm.html) install instructions
|
||||
* [Kubernetes (manual)](https://docs.olivetin.app/install-k8s.html) install instructions
|
||||
* [.tar.gz (manual)](https://docs.olivetin.app/install-targz.html) install instructions
|
||||
|
||||
Put this `config.yaml` in `/etc/OliveTin/` if you're running a standard service, or mount it at `/config` if running in a container.
|
||||
|
||||
```yaml
|
||||
# Listen on all addresses available, port 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
|
||||
- title: Restart Plex
|
||||
icon: restart
|
||||
shell: docker restart plex
|
||||
|
||||
# This will send 1 ping
|
||||
# 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
|
||||
|
||||
- name: count
|
||||
title: Count
|
||||
type: int
|
||||
default: 1
|
||||
|
||||
# Restart http on host "webserver1"
|
||||
# Docs: https://docs.olivetin.app/action-ssh.html
|
||||
- title: restart httpd
|
||||
icon: restart
|
||||
shell: ssh root@webserver1 'service httpd restart'
|
||||
```
|
||||
|
||||
A full example config can be found at in this repository - [config.yaml](https://github.com/OliveTin/OliveTin/blob/main/config.yaml).
|
||||
You can find instructions in the docs on how to install as a **Linux package**, **Linux Container**, on **FreeBSD**, **Windows**, **MacOS** and other platforms, too!
|
||||
|
||||
19
buf.gen.yaml
@@ -1,19 +0,0 @@
|
||||
version: v1
|
||||
plugins:
|
||||
- name: go
|
||||
out: gen/grpc/
|
||||
opt: paths=source_relative
|
||||
|
||||
- name: go-grpc
|
||||
out: gen/grpc/
|
||||
opt: paths=source_relative,require_unimplemented_servers=false
|
||||
|
||||
- name: grpc-gateway
|
||||
out: gen/grpc/
|
||||
opt: paths=source_relative
|
||||
|
||||
# - name: swagger
|
||||
# out: reports/swagger
|
||||
|
||||
# - name: openapiv2
|
||||
# out: reports/openapiv2
|
||||
7
buf.lock
@@ -1,7 +0,0 @@
|
||||
# Generated by buf. DO NOT EDIT.
|
||||
version: v1
|
||||
deps:
|
||||
- remote: buf.build
|
||||
owner: googleapis
|
||||
repository: googleapis
|
||||
commit: e9fcfb66f77242e5b8fd4564d7a01033
|
||||
75
config.yaml
@@ -5,22 +5,31 @@
|
||||
# Listen on all addresses available, port 1337
|
||||
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||
|
||||
bannerMessage: "This is an early alpha version of OliveTin 3000. Many thanks are broken, many things will change."
|
||||
bannerCss: "background-color: #b2e4b2; color: black; font-size: small; text-align: center; padding: .6em; border-radius: 0.5em;"
|
||||
|
||||
insecureAllowDumpSos: true
|
||||
insecureAllowDumpVars: true
|
||||
|
||||
# Choose from INFO (default), WARN and DEBUG
|
||||
logLevel: "INFO"
|
||||
|
||||
# Checking for updates https://docs.olivetin.app/update-checks.html
|
||||
# Checking for updates https://docs.olivetin.app/reference/updateChecks.html
|
||||
checkForUpdates: false
|
||||
|
||||
authLocalUsers:
|
||||
enabled: true
|
||||
|
||||
# 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
|
||||
# Docs: https://docs.olivetin.app/action_execution/create_your_first.html
|
||||
actions:
|
||||
# This is the most simple action, it just runs the command and flashes the
|
||||
# button to indicate status.
|
||||
#
|
||||
# If you are running OliveTin in a container remember to pass through the
|
||||
# docker socket! https://docs.olivetin.app/action-container-control.html
|
||||
# docker socket! https://docs.olivetin.app/solutions/container-control-panel/index.html
|
||||
- title: Ping the Internet
|
||||
shell: ping -c 3 1.1.1.1
|
||||
icon: ping
|
||||
@@ -42,11 +51,16 @@ actions:
|
||||
|
||||
# This uses `popupOnStart: execution-button` to display a mini button that
|
||||
# links to the logs.
|
||||
#
|
||||
# You can also rate-limit actions too.
|
||||
- title: date
|
||||
shell: date
|
||||
timeout: 6
|
||||
icon: clock
|
||||
popupOnStart: execution-button
|
||||
maxRate:
|
||||
- limit: 3
|
||||
duration: 5m
|
||||
|
||||
# You are not limited to operating system commands, and of course you can run
|
||||
# your own scripts. Here `maxConcurrent` stops the script running multiple
|
||||
@@ -63,8 +77,9 @@ actions:
|
||||
# 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
|
||||
# Docs: https://docs.olivetin.app/action_examples/ping.html
|
||||
- title: Ping host
|
||||
id: ping_host
|
||||
shell: ping {{ host }} -c {{ count }}
|
||||
icon: ping
|
||||
timeout: 100
|
||||
@@ -87,10 +102,10 @@ actions:
|
||||
# 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
|
||||
# Docs: https://docs.olivetin.app/solutions/container-control-panel/index.html
|
||||
- title: Restart Docker Container
|
||||
icon: restart
|
||||
shell: docker restart {{ container }}
|
||||
shell: docker restart {{ .CurrentEntity }}
|
||||
arguments:
|
||||
- name: container
|
||||
title: Container name
|
||||
@@ -102,18 +117,22 @@ actions:
|
||||
# There is a special `confirmation` argument to help against accidental clicks
|
||||
# on "dangerous" actions.
|
||||
#
|
||||
# Docs: https://docs.olivetin.app/confirmation.html
|
||||
# Docs: https://docs.olivetin.app/args/input_confirmation.html
|
||||
- title: Delete old backups
|
||||
icon: ashtonished
|
||||
shell: rm -rf /opt/oldBackups/
|
||||
arguments:
|
||||
- type: html
|
||||
title: Description
|
||||
default:
|
||||
The documentation for this action can be found at <a href = "example.com">example.com</a>.
|
||||
- type: confirmation
|
||||
title: Are you sure?!
|
||||
|
||||
# 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
|
||||
# Docs: https://docs.olivetin.app/reference/reference_themes_for_users.html
|
||||
- title: Get OliveTin Theme
|
||||
shell: olivetin-get-theme {{ themeGitRepo }} {{ themeFolderName }}
|
||||
icon: theme
|
||||
@@ -131,8 +150,8 @@ actions:
|
||||
# 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
|
||||
# Docs: https://docs.olivetin.app/action_examples/ssh-easy.html
|
||||
# Docs: https://docs.olivetin.app/action_examples/ssh-manual.html
|
||||
- title: "Setup easy SSH"
|
||||
icon: ssh
|
||||
shell: olivetin-setup-easy-ssh
|
||||
@@ -141,13 +160,13 @@ actions:
|
||||
# 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
|
||||
# Docs: https://docs.olivetin.app/action_examples/ssh-easy.html
|
||||
# Docs: https://docs.olivetin.app/action_examples/systemd_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'
|
||||
shell: ssh -F /config/ssh/easy.cfg 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
|
||||
@@ -160,12 +179,12 @@ actions:
|
||||
# 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
|
||||
# Docs: https://docs.olivetin.app/action_customization/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
|
||||
# Docs: https://docs.olivetin.app/action_examples/ansible.html
|
||||
- title: "Run Automation Playbook"
|
||||
icon: '🤖'
|
||||
shell: ansible-playbook -i /etc/hosts /root/myRepo/myPlaybook.yaml
|
||||
@@ -192,17 +211,17 @@ actions:
|
||||
shell: "echo 'Ping all servers'"
|
||||
icon: ping
|
||||
|
||||
- title: Start {{ container.Names }}
|
||||
- title: Start {{ .CurrentEntity.Names }}
|
||||
icon: box
|
||||
shell: docker start {{ container.Names }}
|
||||
shell: docker start {{ .CurrentEntity.Names }}
|
||||
entity: container
|
||||
trigger: Update container entity file
|
||||
triggers: ["Update container entity file"]
|
||||
|
||||
- title: Stop {{ container.Names }}
|
||||
- title: Stop {{ .CurrentEntity.Names }}
|
||||
icon: box
|
||||
shell: docker stop {{ container.Names }}
|
||||
shell: docker stop {{ .CurrentEntity.Names }}
|
||||
entity: container
|
||||
trigger: Update container entity file
|
||||
triggers: ["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
|
||||
@@ -232,13 +251,13 @@ actions:
|
||||
# in your configuration as variables. For example; `container.status`,
|
||||
# or `vm.hostname`.
|
||||
#
|
||||
# Docs: http://docs.olivetin.app/entities.html
|
||||
# Docs: https://docs.olivetin.app/entities/intro.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
|
||||
# Docs: https://docs.olivetin.app/entities/intro.html
|
||||
- file: entities/servers.yaml
|
||||
name: server
|
||||
|
||||
@@ -250,6 +269,8 @@ entities:
|
||||
#
|
||||
# The only way to properly use entities, are to use them with a `fieldset` on
|
||||
# a dashboard.
|
||||
#
|
||||
# Docs: https://docs.olivetin.app/dashboards/intro.html
|
||||
dashboards:
|
||||
# Top level items are dashboards.
|
||||
- title: My Servers
|
||||
@@ -272,7 +293,7 @@ dashboards:
|
||||
# actions grouped together without a folder.
|
||||
- type: fieldset
|
||||
entity: server
|
||||
title: 'Server: {{ server.hostname }}'
|
||||
title: 'Server: {{ .CurrentEntity.hostname }}'
|
||||
contents:
|
||||
# By default OliveTin will look for an action with a matching title
|
||||
# and put it on the dashboard.
|
||||
@@ -291,7 +312,7 @@ dashboards:
|
||||
# This is the second dashboard.
|
||||
- title: My Containers
|
||||
contents:
|
||||
- title: 'Container {{ container.Names }} ({{ container.Image }})'
|
||||
- title: 'Container {{ .CurrentEntity.Names }} ({{ .CurrentEntity.Image }})'
|
||||
entity: container
|
||||
type: fieldset
|
||||
contents:
|
||||
@@ -299,5 +320,5 @@ dashboards:
|
||||
title: |
|
||||
{{ container.RunningFor }} <br /><br /><strong>{{ container.State }}</strong>
|
||||
|
||||
- title: 'Start {{ container.Names }}'
|
||||
- title: 'Stop {{ container.Names }}'
|
||||
- title: 'Start {{ .CurrentEntity.Names }}'
|
||||
- title: 'Stop {{ .CurrentEntity.Names }}'
|
||||
|
||||
@@ -12,4 +12,4 @@
|
||||
},
|
||||
"rules": {
|
||||
}
|
||||
}
|
||||
}
|
||||
1
frontend/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
fund=false
|
||||
21
frontend/Makefile
Normal file
@@ -0,0 +1,21 @@
|
||||
define delete-files
|
||||
python -c "import shutil;shutil.rmtree('$(1)', ignore_errors=True)"
|
||||
endef
|
||||
|
||||
codestyle:
|
||||
npm install
|
||||
npx eslint --fix main.js js/*
|
||||
npx stylelint style.css
|
||||
|
||||
clean:
|
||||
$(call delete-files,dist)
|
||||
|
||||
deps:
|
||||
npm install
|
||||
|
||||
build:
|
||||
npx vite build
|
||||
|
||||
dist: deps clean build
|
||||
|
||||
.PHONY: codestyle
|
||||
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
69
frontend/index.html
Normal file
@@ -0,0 +1,69 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang = "en">
|
||||
<head>
|
||||
<meta charset = "UTF-8" />
|
||||
<meta name = "viewport" content = "width=device-width, initial-scale=1.0" />
|
||||
<meta name = "description" content = "Give safe and simple access to predefined shell commands from a web interface." />
|
||||
|
||||
<title>OliveTin</title>
|
||||
|
||||
<link rel = "stylesheet" type = "text/css" href = "/theme.css" />
|
||||
<link rel = "stylesheet" href = "node_modules/@xterm/xterm/css/xterm.css" />
|
||||
|
||||
<link rel = "shortcut icon" type = "image/png" href = "OliveTinLogo.png" />
|
||||
|
||||
<link rel = "apple-touch-icon" sizes="57x57" href="OliveTinLogo-57px.png" />
|
||||
<link rel = "apple-touch-icon" sizes="120x120" href="OliveTinLogo-120px.png" />
|
||||
<link rel = "apple-touch-icon" sizes="180x180" href="OliveTinLogo-180px.png" />
|
||||
|
||||
<base href = "/" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<slot id = "app" />
|
||||
|
||||
<noscript>
|
||||
<div class = "error">Sorry, JavaScript is required to use OliveTin.</div>
|
||||
</noscript>
|
||||
|
||||
<dialog title = "Big Error Message" id = "big-error" class = "error padded-content">
|
||||
|
||||
</dialog>
|
||||
|
||||
<script type = "text/javascript">
|
||||
const bigErrorDialog = document.getElementById('big-error')
|
||||
|
||||
/**
|
||||
This is the bootstrap code, which relies on very simple, old javascript
|
||||
to at least display a helpful error message if we can't use OliveTin.
|
||||
*/
|
||||
window.showBigError = function (type, friendlyType, message, isFatal) {
|
||||
console.error('Error ' + type + ': ', message)
|
||||
return;
|
||||
|
||||
bigErrorDialog.innerHTML = '<h1>Error ' + friendlyType + '</h1><p>' + message + "</p><p><a href = 'http://docs.olivetin.app/troubleshooting/err-" + type + ".html' target = 'blank'/>" + type + " error in OliveTin Documentation</a></p>"
|
||||
|
||||
if (isFatal) {
|
||||
bigErrorDialog.innerHTML += '<p>You will need to refresh your browser to clear this message.</p>'
|
||||
} else {
|
||||
bigErrorDialog.innerHTML += '<p>This error message will go away automatically if the problem is solved.</p>'
|
||||
}
|
||||
|
||||
bigErrorDialog.showModal()
|
||||
|
||||
console.error('Error ' + type + ': ', message)
|
||||
}
|
||||
|
||||
window.clearBigErrors = function () {
|
||||
bigErrorDialog.close()
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type = "text/javascript" nomodule>
|
||||
showBigError("js-modules-not-supported", "Sorry, your browser does not support JavaScript modules.", null)
|
||||
</script>
|
||||
|
||||
<script type = "module" src = "main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
24
frontend/js/Mutex.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export class Mutex {
|
||||
constructor () {
|
||||
this._locked = false
|
||||
this._waiting = []
|
||||
}
|
||||
|
||||
lock () {
|
||||
const unlock = () => {
|
||||
const next = this._waiting.shift()
|
||||
if (next) {
|
||||
next(unlock)
|
||||
} else {
|
||||
this._locked = false
|
||||
}
|
||||
}
|
||||
|
||||
if (this._locked) {
|
||||
return new Promise(resolve => this._waiting.push(resolve)).then(() => unlock)
|
||||
} else {
|
||||
this._locked = true
|
||||
return Promise.resolve(unlock)
|
||||
}
|
||||
}
|
||||
}
|
||||
76
frontend/js/OutputTerminal.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import { FitAddon } from '@xterm/addon-fit'
|
||||
import { Mutex } from './Mutex.js'
|
||||
|
||||
/**
|
||||
* xterm.js based terminal output for the execution dialog.
|
||||
*
|
||||
* the xterm.js methods for write(), reset() and clear() appear to be async,
|
||||
* but they do not return a Promise and instead use a callback. When calling
|
||||
* these methods in quick succession, the output can get garbled due to race
|
||||
* conditions.
|
||||
*
|
||||
* To avoid this, this class uses Mutex around those methods to ensure that
|
||||
* only one write OR reset is executed at a time, is completed, and the calls
|
||||
* occour in sequential order.
|
||||
*/
|
||||
export class OutputTerminal {
|
||||
constructor () {
|
||||
this.writeMutex = new Mutex()
|
||||
this.terminal = new Terminal({
|
||||
convertEol: true
|
||||
})
|
||||
|
||||
const fitAddon = new FitAddon()
|
||||
this.terminal.loadAddon(fitAddon)
|
||||
this.terminal.fit = fitAddon
|
||||
}
|
||||
|
||||
async write (out, then) {
|
||||
const unlock = await this.writeMutex.lock()
|
||||
|
||||
try {
|
||||
await new Promise(resolve => {
|
||||
this.terminal.write(out, () => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
} finally {
|
||||
unlock()
|
||||
|
||||
if (then != null && then !== undefined) {
|
||||
then()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async reset () {
|
||||
const unlock = await this.writeMutex.lock()
|
||||
|
||||
try {
|
||||
await new Promise(resolve => {
|
||||
this.terminal.clear()
|
||||
this.terminal.reset()
|
||||
resolve()
|
||||
})
|
||||
} finally {
|
||||
unlock()
|
||||
}
|
||||
}
|
||||
|
||||
fit () {
|
||||
this.terminal.fit.fit()
|
||||
}
|
||||
|
||||
open (el) {
|
||||
this.terminal.open(el)
|
||||
}
|
||||
|
||||
close () {
|
||||
this.terminal.dispose()
|
||||
}
|
||||
|
||||
resize (cols, rows) {
|
||||
this.terminal.resize(cols, rows)
|
||||
}
|
||||
}
|
||||
13
frontend/js/marshaller.js
Normal file
@@ -0,0 +1,13 @@
|
||||
export function initMarshaller () {
|
||||
window.addEventListener('EventOutputChunk', onOutputChunk)
|
||||
}
|
||||
|
||||
function onOutputChunk (evt) {
|
||||
const chunk = evt.payload
|
||||
|
||||
if (window.terminal) {
|
||||
if (chunk.executionTrackingId === window.terminal.executionTrackingId) {
|
||||
window.terminal.write(chunk.output)
|
||||
}
|
||||
}
|
||||
}
|
||||
49
frontend/js/websocket.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { buttonResults } from '../resources/vue/stores/buttonResults.js'
|
||||
|
||||
export function checkWebsocketConnection () {
|
||||
reconnectWebsocket()
|
||||
}
|
||||
|
||||
window.websocketAvailable = false
|
||||
|
||||
async function reconnectWebsocket () {
|
||||
if (window.websocketAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
window.websocketAvailable = true
|
||||
for await (const e of window.client.eventStream()) {
|
||||
handleEvent(e)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Websocket connection failed: ', err)
|
||||
}
|
||||
|
||||
window.websocketAvailable = false
|
||||
console.log('Reconnecting websocket...')
|
||||
}
|
||||
|
||||
function handleEvent (msg) {
|
||||
const typeName = msg.event.value.$typeName.replace('olivetin.api.v1.', '')
|
||||
|
||||
const j = new Event(typeName)
|
||||
j.payload = msg.event.value
|
||||
|
||||
switch (typeName) {
|
||||
case 'EventOutputChunk':
|
||||
case 'EventConfigChanged':
|
||||
case 'EventEntityChanged':
|
||||
window.dispatchEvent(j)
|
||||
break
|
||||
case 'EventExecutionFinished':
|
||||
case 'EventExecutionStarted':
|
||||
buttonResults[msg.event.value.logEntry.executionTrackingId] = msg.event.value.logEntry
|
||||
window.dispatchEvent(j)
|
||||
break
|
||||
default:
|
||||
console.warn('Unhandled websocket message type from server: ', typeName)
|
||||
|
||||
window.showBigError('ws-unhandled-message', 'handling websocket message', 'Unhandled websocket message type from server: ' + typeName, true)
|
||||
}
|
||||
}
|
||||
54
frontend/main.js
Normal file
@@ -0,0 +1,54 @@
|
||||
'use strict'
|
||||
|
||||
import 'femtocrank/style.css'
|
||||
import 'femtocrank/dark.css'
|
||||
import './style.css'
|
||||
|
||||
import 'iconify-icon'
|
||||
|
||||
import { createClient } from '@connectrpc/connect'
|
||||
import { createConnectTransport } from '@connectrpc/connect-web'
|
||||
|
||||
import { OliveTinApiService } from './resources/scripts/gen/olivetin/api/v1/olivetin_pb'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import router from './resources/vue/router.js'
|
||||
import App from './resources/vue/App.vue'
|
||||
|
||||
import {
|
||||
initMarshaller
|
||||
} from './js/marshaller.js'
|
||||
|
||||
import { checkWebsocketConnection } from './js/websocket.js'
|
||||
|
||||
function initClient () {
|
||||
const transport = createConnectTransport({
|
||||
baseUrl: window.location.protocol + '//' + window.location.host + '/api/'
|
||||
|
||||
})
|
||||
|
||||
window.client = createClient(OliveTinApiService, transport)
|
||||
}
|
||||
|
||||
function setupVue () {
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
}
|
||||
|
||||
function main () {
|
||||
initClient()
|
||||
|
||||
// Expose websocket connection function globally so App.vue can call it after successful init
|
||||
window.checkWebsocketConnection = checkWebsocketConnection
|
||||
|
||||
setupVue()
|
||||
|
||||
initMarshaller()
|
||||
|
||||
// window.addEventListener('EventConfigChanged', fetchGetDashboardComponents)
|
||||
// window.addEventListener('EventEntityChanged', fetchGetDashboardComponents)
|
||||
}
|
||||
|
||||
main() // call self
|
||||
3345
frontend/package-lock.json
generated
Normal file
@@ -5,16 +5,9 @@
|
||||
"repository": "https://github.com/OliveTin/OliveTin",
|
||||
"source": "index.html",
|
||||
"devDependencies": {
|
||||
"eslint": "^7.25.0",
|
||||
"eslint-config-standard": "^16.0.2",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.3.1",
|
||||
"parcel": "^2.11.0",
|
||||
"parcel-resolver-ignore": "^2.2.0",
|
||||
"process": "^0.11.10",
|
||||
"stylelint": "^15.6.0",
|
||||
"stylelint-config-standard": "^33.0.0"
|
||||
"stylelint": "^16.25.0",
|
||||
"stylelint-config-standard": "^39.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
@@ -29,7 +22,17 @@
|
||||
],
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@connectrpc/connect": "^2.1.0",
|
||||
"@connectrpc/connect-web": "^2.1.0",
|
||||
"@hugeicons/core-free-icons": "^1.2.1",
|
||||
"@hugeicons/vue": "^1.0.3",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"@xterm/addon-fit": "^0.10.0"
|
||||
"iconify-icon": "^3.0.2",
|
||||
"picocrank": "^1.6.4",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vite": "^7.1.12",
|
||||
"vue-router": "^4.6.3"
|
||||
}
|
||||
}
|
||||
1694
frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts
vendored
Normal file
480
frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js
Normal file
312
frontend/resources/vue/ActionButton.vue
Normal file
@@ -0,0 +1,312 @@
|
||||
<template>
|
||||
<div :id="`actionButton-${actionId}`" role="none" class="action-button">
|
||||
<button :id="`actionButtonInner-${actionId}`" :title="title" :disabled="!canExec || isDisabled"
|
||||
:class="buttonClasses" @click="handleClick">
|
||||
|
||||
<div class="navigate-on-start-container">
|
||||
<div v-if="navigateOnStart == 'pop'" class="navigate-on-start" title="Opens a popup dialog on start">
|
||||
<HugeiconsIcon :icon="ComputerTerminal01Icon" />
|
||||
</div>
|
||||
<div v-if="navigateOnStart == 'arg'" class="navigate-on-start" title="Opens an argument form on start">
|
||||
<HugeiconsIcon :icon="TypeCursorIcon" />
|
||||
</div>
|
||||
<div v-if="navigateOnStart == ''" class="navigate-on-start" title="Run in the background">
|
||||
<HugeiconsIcon :icon="WorkoutRunIcon" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="icon" v-html="unicodeIcon"></span>
|
||||
<span class="title" aria-live="polite">{{ displayTitle }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ArgumentForm from './views/ArgumentForm.vue'
|
||||
import { buttonResults } from './stores/buttonResults'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { HugeiconsIcon } from '@hugeicons/vue'
|
||||
import { WorkoutRunIcon, TypeCursorIcon, ComputerTerminal01Icon } from '@hugeicons/core-free-icons'
|
||||
|
||||
import { ref, watch, onMounted, inject } from 'vue'
|
||||
|
||||
const router = useRouter()
|
||||
const navigateOnStart = ref('')
|
||||
|
||||
const props = defineProps({
|
||||
actionData: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const actionId = ref('')
|
||||
const title = ref('')
|
||||
const canExec = ref(true)
|
||||
const popupOnStart = ref('')
|
||||
|
||||
// Display properties
|
||||
const unicodeIcon = ref('💩')
|
||||
const displayTitle = ref('')
|
||||
|
||||
// State
|
||||
const isDisabled = ref(false)
|
||||
const showArgumentForm = ref(false)
|
||||
|
||||
// Animation classes
|
||||
const buttonClasses = ref([])
|
||||
|
||||
// Timestamps
|
||||
const updateIterationTimestamp = ref(0)
|
||||
|
||||
function getUnicodeIcon(icon) {
|
||||
if (icon === '') {
|
||||
console.log('icon not found ', icon)
|
||||
|
||||
return '💩'
|
||||
} else {
|
||||
return unescape(icon)
|
||||
}
|
||||
}
|
||||
|
||||
function constructFromJson(json) {
|
||||
updateIterationTimestamp.value = 0
|
||||
|
||||
updateFromJson(json)
|
||||
|
||||
actionId.value = json.bindingId
|
||||
title.value = json.title
|
||||
canExec.value = json.canExec
|
||||
popupOnStart.value = json.popupOnStart
|
||||
|
||||
if (popupOnStart.value.includes('execution-dialog')) {
|
||||
navigateOnStart.value = 'pop'
|
||||
} else if (props.actionData.arguments.length > 0) {
|
||||
navigateOnStart.value = 'arg'
|
||||
}
|
||||
|
||||
isDisabled.value = !json.canExec
|
||||
displayTitle.value = title.value
|
||||
unicodeIcon.value = getUnicodeIcon(json.icon)
|
||||
}
|
||||
|
||||
function updateFromJson(json) {
|
||||
// Fields that should not be updated
|
||||
// title - as the callback URL relies on it
|
||||
|
||||
unicodeIcon.value = getUnicodeIcon(json.icon)
|
||||
}
|
||||
|
||||
async function handleClick() {
|
||||
if (props.actionData.arguments && props.actionData.arguments.length > 0) {
|
||||
router.push(`/actionBinding/${props.actionData.bindingId}/argumentForm`)
|
||||
} else {
|
||||
await startAction()
|
||||
}
|
||||
}
|
||||
|
||||
function getUniqueId() {
|
||||
if (window.isSecureContext) {
|
||||
return window.crypto.randomUUID()
|
||||
} else {
|
||||
return Date.now().toString()
|
||||
}
|
||||
}
|
||||
|
||||
async function startAction(actionArgs) {
|
||||
buttonClasses.value = [] // Removes old animation classes
|
||||
|
||||
if (actionArgs === undefined) {
|
||||
actionArgs = []
|
||||
}
|
||||
|
||||
// UUIDs are create client side, so that we can setup a "execution-button"
|
||||
// to track the execution before we send the request to the server.
|
||||
const startActionArgs = {
|
||||
bindingId: props.actionData.bindingId,
|
||||
arguments: actionArgs,
|
||||
uniqueTrackingId: getUniqueId()
|
||||
}
|
||||
|
||||
console.log('Watching buttonResults for', startActionArgs.uniqueTrackingId)
|
||||
|
||||
watch(
|
||||
() => buttonResults[startActionArgs.uniqueTrackingId],
|
||||
(newResult, oldResult) => {
|
||||
onLogEntryChanged(newResult)
|
||||
}
|
||||
)
|
||||
|
||||
try {
|
||||
await window.client.startAction(startActionArgs)
|
||||
} catch (err) {
|
||||
console.error('Failed to start action:', err)
|
||||
}
|
||||
}
|
||||
|
||||
function onLogEntryChanged(logEntry) {
|
||||
if (logEntry.executionFinished) {
|
||||
onExecutionFinished(logEntry)
|
||||
} else {
|
||||
onExecutionStarted(logEntry)
|
||||
}
|
||||
}
|
||||
|
||||
function onExecutionStarted(logEntry) {
|
||||
if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) {
|
||||
router.push(`/logs/${logEntry.executionTrackingId}`)
|
||||
}
|
||||
|
||||
isDisabled.value = true
|
||||
}
|
||||
|
||||
function onExecutionFinished(logEntry) {
|
||||
if (logEntry.timedOut) {
|
||||
renderExecutionResult('action-timeout', 'Timed out')
|
||||
} else if (logEntry.blocked) {
|
||||
renderExecutionResult('action-blocked', 'Blocked!')
|
||||
} else if (logEntry.exitCode !== 0) {
|
||||
renderExecutionResult('action-nonzero-exit', 'Exit code ' + logEntry.exitCode)
|
||||
} else {
|
||||
const ellapsed = Math.ceil(new Date(logEntry.datetimeFinished) - new Date(logEntry.datetimeStarted)) / 1000
|
||||
renderExecutionResult('action-success', 'Success!')
|
||||
}
|
||||
}
|
||||
|
||||
function renderExecutionResult(resultCssClass, temporaryStatusMessage) {
|
||||
updateDom(resultCssClass, '[' + temporaryStatusMessage + ']')
|
||||
onExecStatusChanged()
|
||||
}
|
||||
|
||||
function updateDom(resultCssClass, newTitle) {
|
||||
if (resultCssClass == null) {
|
||||
buttonClasses.value = []
|
||||
} else {
|
||||
buttonClasses.value = [resultCssClass]
|
||||
}
|
||||
|
||||
displayTitle.value = newTitle
|
||||
}
|
||||
|
||||
function onExecStatusChanged() {
|
||||
isDisabled.value = false
|
||||
|
||||
setTimeout(() => {
|
||||
updateDom(null, title.value)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
constructFromJson(props.actionData)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.actionData,
|
||||
(newData) => {
|
||||
updateFromJson(newData)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.action-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.action-button button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
padding: 0.5em;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 0 .6em #aaa;
|
||||
font-size: .85em;
|
||||
border-radius: .7em;
|
||||
}
|
||||
|
||||
.action-button button:hover:not(:disabled) {
|
||||
background: #f5f5f5;
|
||||
border-color: #999;
|
||||
}
|
||||
|
||||
.action-button button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-button button .icon {
|
||||
font-size: 3em;
|
||||
flex-grow: 1;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.action-button button .title {
|
||||
font-weight: 500;
|
||||
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
/* Animation classes */
|
||||
.action-button button.action-timeout {
|
||||
background: #fff3cd;
|
||||
border-color: #ffeaa7;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.action-button button.action-blocked {
|
||||
background: #f8d7da !important;
|
||||
border-color: #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.action-button button.action-nonzero-exit {
|
||||
background: #f8d7da !important;
|
||||
border-color: #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.action-button button.action-success {
|
||||
background: #d4edda !important;
|
||||
border-color: #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.action-button-footer {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.navigate-on-start-container {
|
||||
position: relative;
|
||||
margin-left: auto;
|
||||
height: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.action-button button {
|
||||
background: #111;
|
||||
border-color: #000;
|
||||
box-shadow: 0 0 6px #000;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.action-button button:hover:not(:disabled) {
|
||||
background: #222;
|
||||
border-color: #000;
|
||||
box-shadow: 0 0 6px #444;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
165
frontend/resources/vue/App.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<Header title="OliveTin" :logoUrl="logoUrl" @toggleSidebar="toggleSidebar">
|
||||
<template #toolbar>
|
||||
<div id="banner" v-if="bannerMessage" :style="bannerCss">
|
||||
<p>{{ bannerMessage }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #user-info>
|
||||
<div class="flex-row" style="gap: .5em;">
|
||||
<span id="link-login" v-if="!isLoggedIn"><router-link to="/login">Login</router-link></span>
|
||||
<div v-else>
|
||||
<span id="username-text" :title="'Provider: ' + userProvider">{{ username }}</span>
|
||||
<span id="link-logout" v-if="isLoggedIn"><a href="/api/Logout">Logout</a></span>
|
||||
</div>
|
||||
<HugeiconsIcon :icon="UserCircle02Icon" width = "1.5em" height = "1.5em" />
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</Header>
|
||||
|
||||
<div id="layout">
|
||||
<Sidebar ref="sidebar" id = "mainnav" v-if="showNavigation && !initError" />
|
||||
|
||||
<div id="content" initial-martial-complete="{{ hasLoaded }}">
|
||||
<main title="Main content">
|
||||
<section v-if="initError" class="error-container error" style="text-align: center; padding: 2em;">
|
||||
<h2>Failed to Initialize OliveTin</h2>
|
||||
<p><strong>Error Message:</strong> {{ initErrorMessage }}</p>
|
||||
<p>Please check the your browser console first, and then the server logs for more details.</p>
|
||||
<button @click="retryInit" class="bad">Retry</button>
|
||||
</section>
|
||||
<router-view v-else :key="$route.fullPath" />
|
||||
</main>
|
||||
|
||||
<footer title="footer" v-if="showFooter && !initError">
|
||||
<p>
|
||||
<img title="application icon" :src="logoUrl" alt="OliveTin logo" style="height: 1em;" class="logo" />
|
||||
OliveTin {{ currentVersion }}
|
||||
</p>
|
||||
<p>
|
||||
<span>
|
||||
<a href="https://docs.olivetin.app" target="_new">Documentation</a>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<a href="https://github.com/OliveTin/OliveTin/issues/new/choose" target="_new">Raise an issue on
|
||||
GitHub</a>
|
||||
</span>
|
||||
|
||||
<span>{{ serverConnection }}</span>
|
||||
</p>
|
||||
<p>
|
||||
<a id="available-version" href="http://olivetin.app" target="_blank" hidden>?</a>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import Sidebar from 'picocrank/vue/components/Sidebar.vue';
|
||||
import Header from 'picocrank/vue/components/Header.vue';
|
||||
import { HugeiconsIcon } from '@hugeicons/vue'
|
||||
import { Menu01Icon } from '@hugeicons/core-free-icons'
|
||||
import { UserCircle02Icon } from '@hugeicons/core-free-icons'
|
||||
import { DashboardSquare01Icon } from '@hugeicons/core-free-icons'
|
||||
import logoUrl from '../../OliveTinLogo.png';
|
||||
|
||||
const sidebar = ref(null);
|
||||
const username = ref('guest');
|
||||
const userProvider = ref('system');
|
||||
const isLoggedIn = ref(false);
|
||||
const serverConnection = ref('Connected');
|
||||
const currentVersion = ref('?');
|
||||
const bannerMessage = ref('');
|
||||
const bannerCss = ref('');
|
||||
const hasLoaded = ref(false);
|
||||
const showFooter = ref(true)
|
||||
const showNavigation = ref(true)
|
||||
const showLogs = ref(true)
|
||||
const showDiagnostics = ref(true)
|
||||
const initError = ref(false)
|
||||
const initErrorMessage = ref('')
|
||||
|
||||
function toggleSidebar() {
|
||||
sidebar.value.toggle()
|
||||
}
|
||||
|
||||
async function requestInit() {
|
||||
try {
|
||||
const initResponse = await window.client.init({})
|
||||
|
||||
window.initResponse = initResponse
|
||||
window.initError = false
|
||||
window.initErrorMessage = ''
|
||||
window.initCompleted = true
|
||||
|
||||
username.value = initResponse.authenticatedUser
|
||||
currentVersion.value = initResponse.currentVersion
|
||||
bannerMessage.value = initResponse.bannerMessage || '';
|
||||
bannerCss.value = initResponse.bannerCss || '';
|
||||
showFooter.value = initResponse.showFooter
|
||||
showNavigation.value = initResponse.showNavigation
|
||||
showLogs.value = initResponse.showLogList
|
||||
showDiagnostics.value = initResponse.showDiagnostics
|
||||
|
||||
for (const rootDashboard of initResponse.rootDashboards) {
|
||||
sidebar.value.addNavigationLink({
|
||||
id: rootDashboard,
|
||||
name: rootDashboard,
|
||||
title: rootDashboard,
|
||||
path: rootDashboard === 'Actions' ? '/' : `/dashboards/${rootDashboard}`,
|
||||
icon: DashboardSquare01Icon,
|
||||
})
|
||||
}
|
||||
|
||||
sidebar.value.addSeparator()
|
||||
sidebar.value.addRouterLink('Entities')
|
||||
|
||||
if (showLogs.value) {
|
||||
sidebar.value.addRouterLink('Logs')
|
||||
}
|
||||
|
||||
if (showDiagnostics.value) {
|
||||
sidebar.value.addRouterLink('Diagnostics')
|
||||
}
|
||||
|
||||
hasLoaded.value = true;
|
||||
initError.value = false;
|
||||
|
||||
// Only start websocket connection after successful init
|
||||
if (window.checkWebsocketConnection) {
|
||||
window.checkWebsocketConnection()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error initializing client", error)
|
||||
initError.value = true
|
||||
initErrorMessage.value = error.message || 'Failed to connect to OliveTin server'
|
||||
window.initError = true
|
||||
window.initErrorMessage = error.message || 'Failed to connect to OliveTin server'
|
||||
window.initCompleted = false
|
||||
serverConnection.value = 'Disconnected'
|
||||
}
|
||||
}
|
||||
|
||||
function retryInit() {
|
||||
initError.value = false
|
||||
initErrorMessage.value = ''
|
||||
window.initError = false
|
||||
window.initErrorMessage = ''
|
||||
window.initCompleted = false
|
||||
requestInit()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
serverConnection.value = 'Connected';
|
||||
// Initialize global state
|
||||
window.initError = false
|
||||
window.initErrorMessage = ''
|
||||
window.initCompleted = false
|
||||
requestInit()
|
||||
})
|
||||
</script>
|
||||
174
frontend/resources/vue/Dashboard.vue
Normal file
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<section v-if="!dashboard && !initError" style = "text-align: center; padding: 2em;">
|
||||
<HugeiconsIcon :icon="Loading03Icon" width="3em" height="3em" style="animation: spin 1s linear infinite;" />
|
||||
<p>Loading dashboard...</p>
|
||||
<p style="color: var(--fg2);">{{ loadingTime }}s</p>
|
||||
</section>
|
||||
<section v-if="initError" style="text-align: center; padding: 2em;" class = "bad">
|
||||
<h2 style="color: var(--error);">Initialization Failed</h2>
|
||||
<p>{{ initError }}</p>
|
||||
<p style="color: var(--fg2);">Please check your configuration and try again.</p>
|
||||
</section>
|
||||
<div v-else-if="dashboard">
|
||||
<section v-if="dashboard.contents.length == 0">
|
||||
<legend>{{ dashboard.title }}</legend>
|
||||
<p style = "text-align: center" class = "padding">This dashboard is empty.</p>
|
||||
</section>
|
||||
|
||||
<section class="transparent" v-else>
|
||||
<div v-for="component in dashboard.contents" :key="component.title">
|
||||
<fieldset>
|
||||
<legend v-if = "dashboard.title != 'Default'">{{ component.title }}</legend>
|
||||
|
||||
<template v-for="subcomponent in component.contents">
|
||||
<DashboardComponent :component="subcomponent" />
|
||||
</template>
|
||||
</fieldset>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import DashboardComponent from './components/DashboardComponent.vue'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { HugeiconsIcon } from '@hugeicons/vue'
|
||||
import { Loading03Icon } from '@hugeicons/core-free-icons'
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
})
|
||||
|
||||
const dashboard = ref(null)
|
||||
const loadingTime = ref(0)
|
||||
const initError = ref(null)
|
||||
let loadingTimer = null
|
||||
let checkInitInterval = null
|
||||
|
||||
async function getDashboard() {
|
||||
let title = props.title
|
||||
|
||||
// If no specific title was provided or it's the placeholder 'default',
|
||||
// prefer the first configured root dashboard (e.g., "Test").
|
||||
if ((!title || title === 'default') && window.initResponse.rootDashboards && window.initResponse.rootDashboards.length > 0) {
|
||||
title = window.initResponse.rootDashboards[0]
|
||||
}
|
||||
|
||||
try {
|
||||
const ret = await window.client.getDashboard({
|
||||
title: title,
|
||||
})
|
||||
|
||||
if (!ret || !ret.dashboard) {
|
||||
throw new Error('No dashboard found')
|
||||
}
|
||||
|
||||
dashboard.value = ret.dashboard
|
||||
document.title = ret.dashboard.title + ' - OliveTin'
|
||||
|
||||
// Clear any previous init error since we successfully loaded
|
||||
initError.value = null
|
||||
|
||||
// Stop the loading timer once dashboard is loaded
|
||||
if (loadingTimer) {
|
||||
clearInterval(loadingTimer)
|
||||
loadingTimer = null
|
||||
}
|
||||
|
||||
// Set attribute to indicate dashboard is loaded successfully
|
||||
document.body.setAttribute('loaded-dashboard', title || 'default')
|
||||
} catch (e) {
|
||||
// On error, provide a safe fallback state
|
||||
console.error('Failed to load dashboard', e)
|
||||
dashboard.value = { title: title || 'Default', contents: [] }
|
||||
document.title = 'Error - OliveTin'
|
||||
|
||||
// Stop the loading timer on error
|
||||
if (loadingTimer) {
|
||||
clearInterval(loadingTimer)
|
||||
loadingTimer = null
|
||||
}
|
||||
|
||||
// Set attribute even on error so tests can proceed
|
||||
document.body.setAttribute('loaded-dashboard', title || 'error')
|
||||
}
|
||||
}
|
||||
|
||||
function waitForInitAndLoadDashboard() {
|
||||
// Start the loading timer
|
||||
loadingTime.value = 0
|
||||
loadingTimer = setInterval(() => {
|
||||
loadingTime.value++
|
||||
}, 1000)
|
||||
|
||||
// Check if init has completed successfully
|
||||
if (window.initCompleted && window.initResponse) {
|
||||
getDashboard()
|
||||
} else if (window.initError) {
|
||||
// Init failed, show error immediately
|
||||
initError.value = window.initErrorMessage || 'Initialization failed. Please check your configuration and try again.'
|
||||
// Stop the loading timer since we're showing an error
|
||||
if (loadingTimer) {
|
||||
clearInterval(loadingTimer)
|
||||
loadingTimer = null
|
||||
}
|
||||
} else {
|
||||
// Init hasn't completed yet, poll for completion
|
||||
checkInitInterval = setInterval(() => {
|
||||
if (window.initCompleted && window.initResponse) {
|
||||
clearInterval(checkInitInterval)
|
||||
checkInitInterval = null
|
||||
getDashboard()
|
||||
} else if (window.initError) {
|
||||
clearInterval(checkInitInterval)
|
||||
checkInitInterval = null
|
||||
initError.value = window.initErrorMessage || 'Initialization failed. Please check your configuration and try again.'
|
||||
// Stop the loading timer since we're showing an error
|
||||
if (loadingTimer) {
|
||||
clearInterval(loadingTimer)
|
||||
loadingTimer = null
|
||||
}
|
||||
}
|
||||
}, 100) // Check every 100ms
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
waitForInitAndLoadDashboard()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// Clean up the timers when component is unmounted
|
||||
if (loadingTimer) {
|
||||
clearInterval(loadingTimer)
|
||||
loadingTimer = null
|
||||
}
|
||||
if (checkInitInterval) {
|
||||
clearInterval(checkInitInterval)
|
||||
checkInitInterval = null
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
fieldset {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, 180px);
|
||||
grid-auto-rows: 1fr;
|
||||
justify-content: center;
|
||||
place-items: stretch;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
141
frontend/resources/vue/ExecutionButton.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div
|
||||
:id="`execution-${executionTrackingId}`"
|
||||
class="execution-button"
|
||||
>
|
||||
<button
|
||||
:title="`${ellapsed}s`"
|
||||
@click="show"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//import { ExecutionFeedbackButton } from '../js/ExecutionFeedbackButton.js'
|
||||
|
||||
export default {
|
||||
name: 'ExecutionButton',
|
||||
// mixins: [ExecutionFeedbackButton],
|
||||
props: {
|
||||
executionTrackingId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ellapsed: 0,
|
||||
isWaiting: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
buttonText() {
|
||||
if (this.isWaiting) {
|
||||
return 'Executing...'
|
||||
} else {
|
||||
return `${this.ellapsed}s`
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.constructFromJson(this.executionTrackingId)
|
||||
},
|
||||
methods: {
|
||||
constructFromJson(json) {
|
||||
this.executionTrackingId = json
|
||||
this.ellapsed = 0
|
||||
this.isWaiting = true
|
||||
},
|
||||
|
||||
show() {
|
||||
this.$emit('show')
|
||||
|
||||
if (window.executionDialog) {
|
||||
window.executionDialog.reset()
|
||||
window.executionDialog.show()
|
||||
window.executionDialog.fetchExecutionResult(this.executionTrackingId)
|
||||
}
|
||||
},
|
||||
|
||||
onExecStatusChanged() {
|
||||
this.isWaiting = false
|
||||
this.domTitle = this.ellapsed + 's'
|
||||
},
|
||||
|
||||
// Override from ExecutionFeedbackButton
|
||||
onExecutionFinished(logEntry) {
|
||||
if (logEntry.timedOut) {
|
||||
this.renderExecutionResult('action-timeout', 'Timed out')
|
||||
} else if (logEntry.blocked) {
|
||||
this.renderExecutionResult('action-blocked', 'Blocked!')
|
||||
} else if (logEntry.exitCode !== 0) {
|
||||
this.renderExecutionResult('action-nonzero-exit', 'Exit code ' + logEntry.exitCode)
|
||||
} else {
|
||||
this.ellapsed = Math.ceil(new Date(logEntry.datetimeFinished) - new Date(logEntry.datetimeStarted)) / 1000
|
||||
this.renderExecutionResult('action-success', 'Success!')
|
||||
}
|
||||
},
|
||||
|
||||
renderExecutionResult(resultCssClass, temporaryStatusMessage) {
|
||||
this.updateDom(resultCssClass, '[' + temporaryStatusMessage + ']')
|
||||
this.onExecStatusChanged()
|
||||
},
|
||||
|
||||
updateDom(resultCssClass, title) {
|
||||
// For execution button, we don't need to update classes as much
|
||||
// since it's a simpler component
|
||||
if (resultCssClass) {
|
||||
this.$el.classList.add(resultCssClass)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.execution-button {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.execution-button button {
|
||||
padding: 0.25em 0.5em;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.execution-button button:hover {
|
||||
background: #f5f5f5;
|
||||
border-color: #999;
|
||||
}
|
||||
|
||||
/* Animation classes */
|
||||
.execution-button button.action-timeout {
|
||||
background: #fff3cd;
|
||||
border-color: #ffeaa7;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.execution-button button.action-blocked {
|
||||
background: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.execution-button button.action-nonzero-exit {
|
||||
background: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.execution-button button.action-success {
|
||||
background: #d4edda;
|
||||
border-color: #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
</style>
|
||||
63
frontend/resources/vue/components/ActionStatusDisplay.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<span>
|
||||
<span :class="['action-status', statusClass]">{{ statusText }}</span><span>{{ exitCodeText }}</span>
|
||||
</span>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
logEntry: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const statusText = computed(() => {
|
||||
const logEntry = props.logEntry
|
||||
if (!logEntry) return 'unknown'
|
||||
|
||||
if (logEntry.executionFinished) {
|
||||
if (logEntry.blocked) {
|
||||
return 'Blocked'
|
||||
} else if (logEntry.timedOut) {
|
||||
return 'Timed out'
|
||||
} else {
|
||||
return 'Completed'
|
||||
}
|
||||
} else {
|
||||
return 'Still running...'
|
||||
}
|
||||
})
|
||||
|
||||
const exitCodeText = computed(() => {
|
||||
const logEntry = props.logEntry
|
||||
if (!logEntry) return ''
|
||||
if (logEntry.executionFinished) {
|
||||
if (logEntry.blocked || logEntry.timedOut) {
|
||||
return ''
|
||||
}
|
||||
return ' Exit code: ' + logEntry.exitCode
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const statusClass = computed(() => {
|
||||
const logEntry = props.logEntry
|
||||
if (!logEntry) return ''
|
||||
if (logEntry.executionFinished) {
|
||||
if (logEntry.blocked) {
|
||||
return 'action-blocked'
|
||||
} else if (logEntry.timedOut) {
|
||||
return 'action-timeout'
|
||||
} else if (logEntry.exitCode === 0) {
|
||||
return 'action-success'
|
||||
} else {
|
||||
return 'action-nonzero-exit'
|
||||
}
|
||||
}
|
||||
return ''
|
||||
})
|
||||
</script>
|
||||
58
frontend/resources/vue/components/Breadcrumbs.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div id = "breadcrumbs">
|
||||
<template v-for="(link, index) in links" :key="link.name">
|
||||
<router-link :to="link.href">{{ link.name }}</router-link>
|
||||
<span v-if="index < links.length - 1" class="separator">
|
||||
»
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
span {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
padding: 0.4em;
|
||||
border-radius: 0.2em;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
const links = ref([]);
|
||||
|
||||
watch(() => route.matched, (matched) => {
|
||||
|
||||
links.value = [];
|
||||
matched.forEach((record) => {
|
||||
if (record.meta && record.meta.breadcrumb) {
|
||||
record.meta.breadcrumb.forEach((item) => {
|
||||
links.value.push({
|
||||
name: item.name,
|
||||
href: item.href || record.path || '/'
|
||||
});
|
||||
});
|
||||
} else if (record.name) {
|
||||
links.value.push({
|
||||
name: record.name,
|
||||
href: record.path || '/'
|
||||
});
|
||||
}
|
||||
});
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
41
frontend/resources/vue/components/DashboardComponent.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<ActionButton v-if="component.type == 'link'" :actionData="component.action" :key="component.title" />
|
||||
|
||||
<div v-else-if="component.type == 'directory'">
|
||||
<router-link :to="{ name: 'Dashboard', params: { title: component.title } }" class="dashboard-link">
|
||||
<button>
|
||||
{{ component.title }}
|
||||
</button>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div v-else-if="component.type == 'display'" class="display">
|
||||
<div v-html="component.title" />
|
||||
</div>
|
||||
|
||||
<template v-else-if="component.type == 'fieldset'">
|
||||
<fieldset>
|
||||
<legend>{{ component.title }}</legend>
|
||||
<template v-for="subcomponent in component.contents" :key="subcomponent.title">
|
||||
<DashboardComponent :component="subcomponent" />
|
||||
</template>
|
||||
</fieldset>
|
||||
</template>
|
||||
|
||||
<div v-else>
|
||||
OTHER: {{ component.type }}
|
||||
{{ component }}
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ActionButton from '../ActionButton.vue'
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
284
frontend/resources/vue/components/Pagination.vue
Normal file
@@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<div class="pagination">
|
||||
<div class="pagination-info">
|
||||
<span class="pagination-text">
|
||||
Showing {{ startItem + 1 }}-{{ endItem }} of {{ total }} {{ itemTitle }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="pagination-controls">
|
||||
<button
|
||||
class="pagination-btn"
|
||||
:disabled="currentPage === 1"
|
||||
@click="goToPage(currentPage - 1)"
|
||||
title="Previous page"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M15.41 7.41L14 6l-6 6l6 6l1.41-1.41L10.83 12z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
|
||||
<div class="pagination-pages">
|
||||
<!-- First page -->
|
||||
<button
|
||||
v-if="showFirstPage"
|
||||
class="pagination-btn"
|
||||
:class="{ active: currentPage === 1 }"
|
||||
@click="goToPage(1)"
|
||||
>
|
||||
1
|
||||
</button>
|
||||
|
||||
<!-- Ellipsis after first page -->
|
||||
<span v-if="showFirstEllipsis" class="pagination-ellipsis">...</span>
|
||||
|
||||
<!-- Page numbers around current page -->
|
||||
<button
|
||||
v-for="page in visiblePages"
|
||||
:key="page"
|
||||
class="pagination-btn"
|
||||
:class="{ active: currentPage === page }"
|
||||
@click="goToPage(page)"
|
||||
>
|
||||
{{ page }}
|
||||
</button>
|
||||
|
||||
<!-- Ellipsis before last page -->
|
||||
<span v-if="showLastEllipsis" class="pagination-ellipsis">...</span>
|
||||
|
||||
<!-- Last page -->
|
||||
<button
|
||||
v-if="showLastPage"
|
||||
class="pagination-btn"
|
||||
:class="{ active: currentPage === totalPages }"
|
||||
@click="goToPage(totalPages)"
|
||||
>
|
||||
{{ totalPages }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="pagination-btn"
|
||||
:disabled="currentPage === totalPages"
|
||||
@click="goToPage(currentPage + 1)"
|
||||
title="Next page"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M8.59 16.59L10 18l6-6l-6-6L8.59 7.41L13.17 12z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="pagination-size" v-if="canChangePageSize">
|
||||
<label for="page-size">Items per page:</label>
|
||||
<select
|
||||
id="page-size"
|
||||
v-model="localPageSize"
|
||||
@change="handlePageSizeChange"
|
||||
class="page-size-select"
|
||||
>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 25
|
||||
},
|
||||
total: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
currentPage: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
canChangePageSize: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
itemTitle: {
|
||||
type: String,
|
||||
default: 'items'
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['page-change', 'page-size-change'])
|
||||
|
||||
const localPageSize = ref(props.pageSize)
|
||||
const localCurrentPage = ref(props.currentPage)
|
||||
|
||||
// Computed properties
|
||||
const totalPages = computed(() => Math.ceil(props.total / localPageSize.value))
|
||||
|
||||
const startItem = computed(() => (localCurrentPage.value - 1) * localPageSize.value)
|
||||
const endItem = computed(() => Math.min(localCurrentPage.value * localPageSize.value, props.total))
|
||||
|
||||
// Pagination logic
|
||||
const maxVisiblePages = 5
|
||||
const visiblePages = computed(() => {
|
||||
const pages = []
|
||||
const halfVisible = Math.floor(maxVisiblePages / 2)
|
||||
|
||||
let start = Math.max(1, localCurrentPage.value - halfVisible)
|
||||
let end = Math.min(totalPages.value, start + maxVisiblePages - 1)
|
||||
|
||||
// Adjust start if we're near the end
|
||||
if (end - start < maxVisiblePages - 1) {
|
||||
start = Math.max(1, end - maxVisiblePages + 1)
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
|
||||
return pages
|
||||
})
|
||||
|
||||
const showFirstPage = computed(() => visiblePages.value[0] > 1)
|
||||
const showLastPage = computed(() => visiblePages.value[visiblePages.value.length - 1] < totalPages.value)
|
||||
const showFirstEllipsis = computed(() => visiblePages.value[0] > 2)
|
||||
const showLastEllipsis = computed(() => visiblePages.value[visiblePages.value.length - 1] < totalPages.value - 1)
|
||||
|
||||
// Methods
|
||||
function goToPage(page) {
|
||||
if (page >= 1 && page <= totalPages.value && page !== localCurrentPage.value) {
|
||||
localCurrentPage.value = page
|
||||
emit('page-change', page)
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageSizeChange() {
|
||||
// Reset to first page when changing page size
|
||||
localCurrentPage.value = 1
|
||||
emit('page-size-change', localPageSize.value)
|
||||
emit('page-change', 1)
|
||||
}
|
||||
|
||||
// Watch for prop changes
|
||||
watch(() => props.currentPage, (newPage) => {
|
||||
localCurrentPage.value = newPage
|
||||
})
|
||||
|
||||
watch(() => props.pageSize, (newSize) => {
|
||||
localPageSize.value = newSize
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.pagination-text {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.pagination-pages {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.pagination-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #dee2e6;
|
||||
background: #fff;
|
||||
color: #495057;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.pagination-btn:hover:not(:disabled) {
|
||||
background: #e9ecef;
|
||||
border-color: #adb5bd;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.pagination-btn.active {
|
||||
background: #c6d0d7;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.pagination-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.pagination-ellipsis {
|
||||
padding: 0.5rem;
|
||||
color: #6c757d;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.pagination-size {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.page-size-select {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.page-size-select:focus {
|
||||
outline: none;
|
||||
border-color: #5681af;
|
||||
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.pagination {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pagination-size {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
129
frontend/resources/vue/router.js
Normal file
@@ -0,0 +1,129 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
import { Wrench01Icon } from '@hugeicons/core-free-icons'
|
||||
import { LeftToRightListDashIcon } from '@hugeicons/core-free-icons'
|
||||
import { CellsIcon } from '@hugeicons/core-free-icons'
|
||||
import { DashboardSquare01Icon } from '@hugeicons/core-free-icons'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Actions',
|
||||
component: () => import('./Dashboard.vue'),
|
||||
meta: { title: 'Actions', icon: DashboardSquare01Icon }
|
||||
},
|
||||
{
|
||||
path: '/dashboards/:title',
|
||||
name: 'Dashboard',
|
||||
component: () => import('./Dashboard.vue'),
|
||||
props: true,
|
||||
meta: { title: 'Dashboard' }
|
||||
},
|
||||
{
|
||||
path: '/actionBinding/:bindingId/argumentForm',
|
||||
name: 'ActionBinding',
|
||||
component: () => import('./views/ArgumentForm.vue'),
|
||||
props: true,
|
||||
meta: { title: 'Action Binding' }
|
||||
},
|
||||
{
|
||||
path: '/logs',
|
||||
name: 'Logs',
|
||||
component: () => import('./views/LogsListView.vue'),
|
||||
meta: {
|
||||
title: 'Logs',
|
||||
icon: LeftToRightListDashIcon
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/entities',
|
||||
name: 'Entities',
|
||||
component: () => import('./views/EntitiesView.vue'),
|
||||
meta: {
|
||||
title: 'Entities',
|
||||
icon: CellsIcon
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/entity-details/:entityType/:entityKey',
|
||||
name: 'EntityDetails',
|
||||
component: () => import('./views/EntityDetailsView.vue'),
|
||||
props: true,
|
||||
meta: {
|
||||
title: 'OliveTin - Entity Details',
|
||||
breadcrumb: [
|
||||
{ name: "Entities", href: "/entities" },
|
||||
{ name: "Entity Details" }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/logs/:executionTrackingId',
|
||||
name: 'Execution',
|
||||
component: () => import('./views/ExecutionView.vue'),
|
||||
props: true,
|
||||
meta: {
|
||||
title: 'Execution',
|
||||
breadcrumb: [
|
||||
{ name: "Logs", href: "/logs" },
|
||||
{ name: "Execution" },
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/diagnostics',
|
||||
name: 'Diagnostics',
|
||||
component: () => import('./views/DiagnosticsView.vue'),
|
||||
meta: {
|
||||
title: 'Diagnostics',
|
||||
icon: Wrench01Icon
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('./views/LoginView.vue'),
|
||||
meta: { title: 'Login' }
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
component: () => import('./views/NotFoundView.vue'),
|
||||
meta: { title: 'Page Not Found' }
|
||||
}
|
||||
]
|
||||
|
||||
// Create router instance
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
if (savedPosition) {
|
||||
return savedPosition
|
||||
} else {
|
||||
return { top: 0 }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Navigation guard to update page title
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.meta && to.meta.title) {
|
||||
document.title = to.meta.title + " - OliveTin"
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
// Navigation guard for authentication (if needed)
|
||||
router.beforeEach((to, from, next) => {
|
||||
// Check if user is authenticated for protected routes
|
||||
const isAuthenticated = window.isAuthenticated || true // Default to true for now
|
||||
|
||||
if (to.meta.requiresAuth && !isAuthenticated) {
|
||||
next('/login')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
3
frontend/resources/vue/stores/buttonResults.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { reactive } from 'vue'
|
||||
|
||||
export const buttonResults = reactive({})
|
||||
392
frontend/resources/vue/views/ArgumentForm.vue
Normal file
@@ -0,0 +1,392 @@
|
||||
<template>
|
||||
<section id = "argument-popup">
|
||||
<div class="section-header">
|
||||
<h2>Start action: {{ title }}</h2>
|
||||
</div>
|
||||
<div class="section-content padding">
|
||||
<form @submit="handleSubmit">
|
||||
<template v-if="actionArguments.length > 0">
|
||||
|
||||
<template v-for="arg in actionArguments" :key="arg.name" class="argument-group">
|
||||
<label :for="arg.name">
|
||||
{{ formatLabel(arg.title) }}
|
||||
</label>
|
||||
|
||||
<datalist v-if="arg.suggestions && Object.keys(arg.suggestions).length > 0" :id="`${arg.name}-choices`">
|
||||
<option v-for="(suggestion, key) in arg.suggestions" :key="key" :value="key">
|
||||
{{ suggestion }}
|
||||
</option>
|
||||
</datalist>
|
||||
|
||||
<select v-if="getInputComponent(arg) === 'select'" :id="arg.name" :name="arg.name" :value="getArgumentValue(arg)"
|
||||
:required="arg.required" @input="handleInput(arg, $event)" @change="handleChange(arg, $event)">
|
||||
<option v-for="choice in arg.choices" :key="choice.value" :value="choice.value">
|
||||
{{ choice.title || choice.value }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<component v-else :is="getInputComponent(arg)" :id="arg.name" :name="arg.name" :value="getArgumentValue(arg)"
|
||||
:list="arg.suggestions ? `${arg.name}-choices` : undefined"
|
||||
:type="getInputComponent(arg) !== 'select' ? getInputType(arg) : undefined"
|
||||
:rows="arg.type === 'raw_string_multiline' ? 5 : undefined"
|
||||
:step="arg.type === 'datetime' ? 1 : undefined" :pattern="getPattern(arg)" :required="arg.required"
|
||||
@input="handleInput(arg, $event)" @change="handleChange(arg, $event)" />
|
||||
|
||||
<span class="argument-description" v-html="arg.description"></span>
|
||||
</template>
|
||||
</template>
|
||||
<div v-else>
|
||||
<p>No arguments required</p>
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
<button name="start" type="submit" :disabled="hasConfirmation && !confirmationChecked">
|
||||
Start
|
||||
</button>
|
||||
<button name="cancel" type="button" @click="handleCancel">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// Reactive data
|
||||
const dialog = ref(null)
|
||||
const title = ref('')
|
||||
const icon = ref('')
|
||||
//const arguments = ref([])
|
||||
const argValues = ref({})
|
||||
const confirmationChecked = ref(false)
|
||||
const hasConfirmation = ref(false)
|
||||
const formErrors = ref({})
|
||||
const actionArguments = ref([])
|
||||
|
||||
// Computed properties
|
||||
|
||||
const props = defineProps({
|
||||
bindingId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
// Methods
|
||||
async function setup() {
|
||||
const ret = await window.client.getActionBinding({
|
||||
bindingId: props.bindingId
|
||||
})
|
||||
|
||||
const action = ret.action
|
||||
|
||||
title.value = action.title
|
||||
icon.value = action.icon
|
||||
actionArguments.value = action.arguments || []
|
||||
argValues.value = {}
|
||||
formErrors.value = {}
|
||||
confirmationChecked.value = false
|
||||
hasConfirmation.value = false
|
||||
|
||||
// Initialize values from query params or defaults
|
||||
actionArguments.value.forEach(arg => {
|
||||
const paramValue = getQueryParamValue(arg.name)
|
||||
argValues.value[arg.name] = paramValue !== null ? paramValue : arg.defaultValue || ''
|
||||
|
||||
if (arg.type === 'confirmation') {
|
||||
hasConfirmation.value = true
|
||||
}
|
||||
})
|
||||
|
||||
// Run initial validation on all fields after DOM is updated
|
||||
await nextTick()
|
||||
for (const arg of actionArguments.value) {
|
||||
if (arg.type && !arg.type.startsWith('regex:') && arg.type !== 'select' && arg.type !== '') {
|
||||
await validateArgument(arg, argValues.value[arg.name])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getQueryParamValue(paramName) {
|
||||
const params = new URLSearchParams(window.location.search.substring(1))
|
||||
return params.get(paramName)
|
||||
}
|
||||
|
||||
function formatLabel(title) {
|
||||
const lastChar = title.charAt(title.length - 1)
|
||||
if (lastChar === '?' || lastChar === '.' || lastChar === ':') {
|
||||
return title
|
||||
}
|
||||
return title + ':'
|
||||
}
|
||||
|
||||
function getInputComponent(arg) {
|
||||
if (arg.type === 'html') {
|
||||
return 'div'
|
||||
} else if (arg.type === 'raw_string_multiline') {
|
||||
return 'textarea'
|
||||
} else if (arg.choices && arg.choices.length > 0 && (arg.type === 'select' || arg.type === '')) {
|
||||
return 'select'
|
||||
} else {
|
||||
return 'input'
|
||||
}
|
||||
}
|
||||
|
||||
function getInputType(arg) {
|
||||
if (arg.type === 'html' || arg.type === 'raw_string_multiline' || arg.type === 'select') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (arg.type === 'ascii_identifier') {
|
||||
return 'text'
|
||||
}
|
||||
|
||||
return arg.type
|
||||
}
|
||||
|
||||
function getPattern(arg) {
|
||||
if (arg.type && arg.type.startsWith('regex:')) {
|
||||
return arg.type.replace('regex:', '')
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function getArgumentValue(arg) {
|
||||
if (arg.type === 'checkbox') {
|
||||
return argValues.value[arg.name] === '1' || argValues.value[arg.name] === true
|
||||
}
|
||||
return argValues.value[arg.name] || ''
|
||||
}
|
||||
|
||||
function handleInput(arg, event) {
|
||||
const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value
|
||||
argValues.value[arg.name] = value
|
||||
updateUrlWithArg(arg.name, value)
|
||||
}
|
||||
|
||||
function handleChange(arg, event) {
|
||||
if (arg.type === 'confirmation') {
|
||||
confirmationChecked.value = event.target.checked
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the input
|
||||
validateArgument(arg, event.target.value)
|
||||
}
|
||||
|
||||
async function validateArgument(arg, value) {
|
||||
if (!arg.type || arg.type.startsWith('regex:')) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const validateArgumentTypeArgs = {
|
||||
value: value,
|
||||
type: arg.type
|
||||
}
|
||||
|
||||
const validation = await window.client.validateArgumentType(validateArgumentTypeArgs)
|
||||
|
||||
// Get the input element to set custom validity
|
||||
const inputElement = document.getElementById(arg.name)
|
||||
|
||||
if (validation.valid) {
|
||||
delete formErrors.value[arg.name]
|
||||
// Clear custom validity message
|
||||
if (inputElement) {
|
||||
inputElement.setCustomValidity('')
|
||||
}
|
||||
} else {
|
||||
formErrors.value[arg.name] = validation.description
|
||||
// Set custom validity message
|
||||
if (inputElement) {
|
||||
inputElement.setCustomValidity(validation.description)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Validation failed:', err)
|
||||
// On error, clear any custom validity
|
||||
const inputElement = document.getElementById(arg.name)
|
||||
if (inputElement) {
|
||||
inputElement.setCustomValidity('')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateUrlWithArg(name, value) {
|
||||
if (name && value !== undefined) {
|
||||
const url = new URL(window.location.href)
|
||||
|
||||
// Don't add passwords to URL
|
||||
const arg = actionArguments.value.find(a => a.name === name)
|
||||
if (arg && arg.type === 'password') {
|
||||
return
|
||||
}
|
||||
|
||||
url.searchParams.set(name, value)
|
||||
window.history.replaceState({}, '', url.toString())
|
||||
}
|
||||
}
|
||||
|
||||
function getArgumentValues() {
|
||||
const ret = []
|
||||
|
||||
for (const arg of actionArguments.value) {
|
||||
let value = argValues.value[arg.name] || ''
|
||||
|
||||
if (arg.type === 'checkbox') {
|
||||
value = value ? '1' : '0'
|
||||
}
|
||||
|
||||
ret.push({
|
||||
name: arg.name,
|
||||
value: value
|
||||
})
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
function getUniqueId() {
|
||||
if (window.isSecureContext) {
|
||||
return window.crypto.randomUUID()
|
||||
} else {
|
||||
return Date.now().toString()
|
||||
}
|
||||
}
|
||||
|
||||
async function startAction(actionArgs) {
|
||||
const startActionArgs = {
|
||||
bindingId: props.bindingId,
|
||||
arguments: actionArgs,
|
||||
uniqueTrackingId: getUniqueId()
|
||||
}
|
||||
|
||||
try {
|
||||
await window.client.startAction(startActionArgs)
|
||||
console.log('Action started successfully with tracking ID:', startActionArgs.uniqueTrackingId)
|
||||
} catch (err) {
|
||||
console.error('Failed to start action:', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit(event) {
|
||||
// Set custom validity for required fields
|
||||
for (const arg of actionArguments.value) {
|
||||
const value = argValues.value[arg.name]
|
||||
const inputElement = document.getElementById(arg.name)
|
||||
|
||||
if (arg.required && (!value || value === '')) {
|
||||
formErrors.value[arg.name] = 'This field is required'
|
||||
// Set custom validity for required field validation
|
||||
if (inputElement) {
|
||||
inputElement.setCustomValidity('This field is required')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const form = event.target
|
||||
if (!form.checkValidity()) {
|
||||
console.log('argument form has elements that failed validation')
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
const argvs = getArgumentValues()
|
||||
console.log('argument form has elements that passed validation')
|
||||
|
||||
await startAction(argvs)
|
||||
|
||||
router.back()
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
router.back()
|
||||
clearBookmark()
|
||||
}
|
||||
|
||||
function clearBookmark() {
|
||||
window.history.replaceState({
|
||||
path: window.location.pathname
|
||||
}, '', window.location.pathname)
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (dialog.value) {
|
||||
dialog.value.showModal()
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (dialog.value) {
|
||||
dialog.value.close()
|
||||
}
|
||||
}
|
||||
|
||||
// Expose methods for parent components
|
||||
defineExpose({
|
||||
show,
|
||||
close
|
||||
})
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
setup()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
form {
|
||||
grid-template-columns: max-content auto auto;
|
||||
}
|
||||
|
||||
.argument-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.argument-group label {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.argument-group input:invalid,
|
||||
.argument-group select:invalid,
|
||||
.argument-group textarea:invalid {
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.argument-description {
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-end;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
/* Checkbox specific styling */
|
||||
.argument-group input[type="checkbox"] {
|
||||
width: auto;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.argument-group input[type="checkbox"]+label {
|
||||
display: inline;
|
||||
font-weight: normal;
|
||||
}
|
||||
</style>
|
||||
158
frontend/resources/vue/views/DiagnosticsView.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<Section title = "Get support">
|
||||
<p>If you are having problems with OliveTin and want to raise a support request, it would be very helpful to include a sosreport from this page.
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://docs.olivetin.app/sosreport.html" target="_blank">sosreport Documentation</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href = "https://docs.olivetin.app/troubleshooting/wheretofindhelp.html" target="_blank">Where to find help</a>
|
||||
</li>
|
||||
</ul>
|
||||
</Section>
|
||||
|
||||
<Section title = "SSH">
|
||||
<dl>
|
||||
<dt>Found Key</dt>
|
||||
<dd>{{ diagnostics.sshFoundKey || '?' }}</dd>
|
||||
<dt>Found Config</dt>
|
||||
<dd>{{ diagnostics.sshFoundConfig || '?' }}</dd>
|
||||
</dl>
|
||||
</Section>
|
||||
|
||||
<Section title = "SOS Report">
|
||||
<p>This section allows you to generate a detailed report of your configuration and environment. It is a good idea to include this when raising a support request.</p>
|
||||
|
||||
<div role="toolbar">
|
||||
<button @click="generateSosReport" :disabled="loading" class = "good">Generate SOS Report</button>
|
||||
</div>
|
||||
|
||||
<textarea v-model="sosReport" readonly style="flex: 1; min-height: 200px; resize: vertical;"></textarea>
|
||||
</Section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import Section from 'picocrank/vue/components/Section.vue'
|
||||
|
||||
const diagnostics = ref({})
|
||||
const loading = ref(false)
|
||||
const sosReport = ref('Waiting to start...')
|
||||
|
||||
async function fetchDiagnostics() {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const response = await window.client.getDiagnostics();
|
||||
diagnostics.value = {
|
||||
sshFoundKey: response.SshFoundKey,
|
||||
sshFoundConfig: response.SshFoundConfig
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch diagnostics:', err);
|
||||
diagnostics.value = {
|
||||
sshFoundKey: 'Unknown',
|
||||
sshFoundConfig: 'Unknown'
|
||||
}
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
function formatKey(key) {
|
||||
return key
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/^./, str => str.toUpperCase())
|
||||
.trim()
|
||||
}
|
||||
|
||||
async function generateSosReport() {
|
||||
const response = await window.client.sosReport()
|
||||
console.log("response", response)
|
||||
sosReport.value = response.alert
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchDiagnostics()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.diagnostics-view {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.diagnostics-content {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.note {
|
||||
background: #f8f9fa;
|
||||
border-left: 4px solid #007bff;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 0 4px 4px 0;
|
||||
font-size: 0.875rem;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.note a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.note a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.diagnostics-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.diagnostics-table td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid #f1f3f4;
|
||||
}
|
||||
|
||||
.diagnostics-table td:first-child {
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.diagnostics-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.error-list {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.error-item {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #dc3545;
|
||||
font-family: monospace;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.error-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
}
|
||||
</style>
|
||||
50
frontend/resources/vue/views/EntitiesView.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<Section class = "with-header-and-content" v-if="entityDefinitions.length === 0" title="Loading entity definitions...">
|
||||
<div class = "section-header">
|
||||
<h2 class="loading-message">
|
||||
Loading entity definitions...
|
||||
</h2>
|
||||
</div>
|
||||
</Section>
|
||||
<template v-else>
|
||||
<Section v-for="def in entityDefinitions" :key="def.name" :title="'Entity: ' + def.title ">
|
||||
<div class = "section-content">
|
||||
<p>{{ def.instances.length }} instances.</p>
|
||||
|
||||
<ul>
|
||||
<li v-for="inst in def.instances" :key="inst.id">
|
||||
<router-link :to="{ name: 'EntityDetails', params: { entityType: inst.type, entityKey: inst.uniqueKey } }">
|
||||
{{ inst.title }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3>Used on Dashboards:</h3>
|
||||
<ul>
|
||||
<li v-for="dash in def.usedOnDashboards">
|
||||
<router-link :to="{ name: 'Dashboard', params: { title: dash } }">
|
||||
{{ dash }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Section>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import Section from 'picocrank/vue/components/Section.vue'
|
||||
|
||||
const entityDefinitions = ref([])
|
||||
|
||||
async function fetchEntities() {
|
||||
const ret = await window.client.getEntities()
|
||||
|
||||
entityDefinitions.value = ret.entityDefinitions
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchEntities()
|
||||
})
|
||||
</script>
|
||||
40
frontend/resources/vue/views/EntityDetailsView.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<Section title="Entity Details">
|
||||
<div>
|
||||
<p v-if="!entityDetails">Loading entity details...</p>
|
||||
<p v-else-if="!entityDetails.title">No details available for this entity.</p>
|
||||
<p v-else>{{ entityDetails.title }}</p>
|
||||
</div>
|
||||
</Section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import Section from 'picocrank/vue/components/Section.vue'
|
||||
|
||||
const entityDetails = ref(null)
|
||||
|
||||
const props = defineProps({
|
||||
entityType: String,
|
||||
entityKey: String
|
||||
})
|
||||
|
||||
async function fetchEntityDetails() {
|
||||
try {
|
||||
const response = await window.client.getEntity({
|
||||
type: props.entityType,
|
||||
uniqueKey: props.entityKey
|
||||
})
|
||||
|
||||
entityDetails.value = response
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch entity details:', err)
|
||||
window.showBigError('fetch-entity-details', 'getting entity details', err, false)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchEntityDetails()
|
||||
})
|
||||
|
||||
</script>
|
||||
310
frontend/resources/vue/views/ExecutionView.vue
Normal file
@@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<Section :title="'Execution Results: ' + title" id = "execution-results-popup">
|
||||
<template #toolbar>
|
||||
<button @click="toggleSize" title="Toggle dialog size">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||
<path fill="currentColor"
|
||||
d="M3 3h6v2H6.462l4.843 4.843l-1.415 1.414L5 6.367V9H3zm0 18h6v-2H6.376l4.929-4.928l-1.415-1.414L5 17.548V15H3zm12 0h6v-6h-2v2.524l-4.867-4.866l-1.414 1.414L17.647 19H15zm6-18h-6v2h2.562l-4.843 4.843l1.414 1.414L19 6.39V9h2z" />
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<div v-if="logEntry" class = "flex-row">
|
||||
<dl class = "fg1">
|
||||
<dt>Duration</dt>
|
||||
<dd><span v-html="duration"></span></dd>
|
||||
|
||||
<dt>Status</dt>
|
||||
<dd>
|
||||
<ActionStatusDisplay :log-entry="logEntry" id = "execution-dialog-status" />
|
||||
</dd>
|
||||
</dl>
|
||||
<span class="icon" role="img" v-html="icon" style = "align-self: start"></span>
|
||||
</div>
|
||||
|
||||
<div ref="xtermOutput"></div>
|
||||
|
||||
<br />
|
||||
|
||||
<div class="flex-row g1 buttons padded-content">
|
||||
<button @click="goBack" title="Go back">
|
||||
<HugeiconsIcon :icon="ArrowLeftIcon" />
|
||||
Back
|
||||
</button>
|
||||
|
||||
<div class = "fg1" />
|
||||
|
||||
<button :disabled="!canRerun" @click="rerunAction" title="Rerun">
|
||||
<HugeiconsIcon :icon="WorkoutRunIcon" />
|
||||
Rerun
|
||||
</button>
|
||||
<button :disabled="!canKill" @click="killAction" title="Kill" id = "execution-dialog-kill-action">
|
||||
<HugeiconsIcon :icon="Cancel02Icon" />
|
||||
Kill
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</Section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import ActionStatusDisplay from '../components/ActionStatusDisplay.vue'
|
||||
import Section from 'picocrank/vue/components/Section.vue'
|
||||
import { OutputTerminal } from '../../../js/OutputTerminal.js'
|
||||
import { HugeiconsIcon } from '@hugeicons/vue'
|
||||
import { WorkoutRunIcon, Cancel02Icon, ArrowLeftIcon } from '@hugeicons/core-free-icons'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { buttonResults } from '../stores/buttonResults'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// Refs for DOM elements
|
||||
const xtermOutput = ref(null)
|
||||
|
||||
const props = defineProps({
|
||||
executionTrackingId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const executionTrackingId = ref(props.executionTrackingId)
|
||||
const hideBasics = ref(false)
|
||||
const hideDetails = ref(false)
|
||||
const hideDetailsOnResult = ref(false)
|
||||
const executionSeconds = ref(0)
|
||||
const icon = ref('')
|
||||
const title = ref('Waiting for result...')
|
||||
const titleTooltip = ref('')
|
||||
const duration = ref('')
|
||||
const logEntry = ref(null)
|
||||
const canRerun = ref(false)
|
||||
const canKill = ref(false)
|
||||
|
||||
let executionTicker = null
|
||||
let terminal = null
|
||||
|
||||
function initializeTerminal() {
|
||||
terminal = new OutputTerminal(executionTrackingId.value, this)
|
||||
terminal.open(xtermOutput.value)
|
||||
terminal.resize(80, 24)
|
||||
|
||||
window.terminal = terminal
|
||||
}
|
||||
|
||||
function toggleSize() {
|
||||
terminal.fit()
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
executionSeconds.value = 0
|
||||
executionTrackingId.value = 'notset'
|
||||
hideBasics.value = false
|
||||
hideDetails.value = false
|
||||
hideDetailsOnResult.value = false
|
||||
|
||||
icon.value = ''
|
||||
title.value = 'Waiting for result...'
|
||||
titleTooltip.value = ''
|
||||
duration.value = ''
|
||||
|
||||
canRerun.value = false
|
||||
canKill.value = false
|
||||
logEntry.value = null
|
||||
|
||||
if (terminal) {
|
||||
await terminal.reset()
|
||||
terminal.fit()
|
||||
}
|
||||
}
|
||||
|
||||
function show(actionButton) {
|
||||
if (actionButton) {
|
||||
icon.value = actionButton.domIcon.innerText
|
||||
}
|
||||
|
||||
canKill.value = true
|
||||
|
||||
// Clear existing ticker
|
||||
if (executionTicker) {
|
||||
clearInterval(executionTicker)
|
||||
}
|
||||
|
||||
executionSeconds.value = 0
|
||||
executionTick()
|
||||
executionTicker = setInterval(() => {
|
||||
executionTick()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
async function rerunAction() {
|
||||
let startActionArgs = {}
|
||||
const res = await window.client.startAction(startActionArgs)
|
||||
|
||||
router.push(`/logs/${res.executionTrackingId}`)
|
||||
}
|
||||
|
||||
async function killAction() {
|
||||
if (!executionTrackingId.value || executionTrackingId.value === 'notset') {
|
||||
return
|
||||
}
|
||||
|
||||
const killActionArgs = {
|
||||
executionTrackingId: executionTrackingId.value
|
||||
}
|
||||
|
||||
try {
|
||||
await window.client.killAction(killActionArgs)
|
||||
} catch (err) {
|
||||
console.error('Failed to kill action:', err)
|
||||
}
|
||||
}
|
||||
|
||||
function executionTick() {
|
||||
executionSeconds.value++
|
||||
updateDuration(null)
|
||||
}
|
||||
|
||||
function hideEverythingApartFromOutput() {
|
||||
hideDetailsOnResult.value = true
|
||||
hideBasics.value = true
|
||||
hideDetailsOnResult.value = true
|
||||
hideBasics.value = true
|
||||
}
|
||||
|
||||
async function fetchExecutionResult(executionTrackingIdParam) {
|
||||
console.log("fetchExecutionResult", executionTrackingIdParam)
|
||||
|
||||
executionTrackingId.value = executionTrackingIdParam
|
||||
|
||||
const executionStatusArgs = {
|
||||
executionTrackingId: executionTrackingId.value
|
||||
}
|
||||
|
||||
try {
|
||||
const logEntryResult = await window.client.executionStatus(executionStatusArgs)
|
||||
|
||||
await renderExecutionResult(logEntryResult)
|
||||
} catch (err) {
|
||||
renderError(err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
function updateDuration(logEntryParam) {
|
||||
logEntry.value = logEntryParam
|
||||
if (logEntry.value == null) {
|
||||
duration.value = executionSeconds.value + ' seconds'
|
||||
duration.value = duration.value
|
||||
} else if (!logEntry.value.executionStarted) {
|
||||
duration.value = logEntry.value.datetimeStarted + ' (request time). Not executed.'
|
||||
} else if (logEntry.value.executionStarted && !logEntry.value.executionFinished) {
|
||||
duration.value = logEntry.value.datetimeStarted
|
||||
} else {
|
||||
let delta = ''
|
||||
try {
|
||||
delta = (new Date(logEntry.value.datetimeStarted) - new Date(logEntry.value.datetimeStarted)) / 1000
|
||||
delta = new Intl.RelativeTimeFormat().format(delta, 'seconds').replace('in ', '').replace('ago', '')
|
||||
} catch (e) {
|
||||
console.warn('Failed to calculate delta', e)
|
||||
}
|
||||
duration.value = logEntry.value.datetimeStarted + ' → ' + logEntry.value.datetimeFinished
|
||||
if (delta !== '') {
|
||||
duration.value += ' (' + delta + ')'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function renderExecutionResult(res) {
|
||||
logEntry.value = res.logEntry
|
||||
|
||||
// Clear ticker
|
||||
if (executionTicker) {
|
||||
clearInterval(executionTicker)
|
||||
}
|
||||
executionTicker = null
|
||||
|
||||
if (hideDetailsOnResult.value) {
|
||||
hideDetails.value = true
|
||||
}
|
||||
|
||||
executionTrackingId.value = res.logEntry.executionTrackingId
|
||||
canRerun.value = res.logEntry.executionFinished
|
||||
canKill.value = res.logEntry.canKill
|
||||
|
||||
icon.value = res.logEntry.actionIcon
|
||||
title.value = res.logEntry.actionTitle
|
||||
titleTooltip.value = 'Action ID: ' + res.logEntry.actionId + '\nExecution ID: ' + res.logEntry.executionTrackingId
|
||||
|
||||
updateDuration(res.logEntry)
|
||||
|
||||
if (terminal) {
|
||||
await terminal.reset()
|
||||
await terminal.write(res.logEntry.output, () => {
|
||||
terminal.fit()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function renderError(err) {
|
||||
window.showBigError('execution-dlg-err', 'in the execution dialog', 'Failed to fetch execution result. ' + err, false)
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
if (executionTicker) {
|
||||
clearInterval(executionTicker)
|
||||
}
|
||||
|
||||
executionTicker = null
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
if (executionTicker) {
|
||||
clearInterval(executionTicker)
|
||||
}
|
||||
executionTicker = null
|
||||
if (terminal != null) {
|
||||
terminal.close()
|
||||
}
|
||||
terminal = null
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initializeTerminal()
|
||||
fetchExecutionResult(props.executionTrackingId)
|
||||
|
||||
watch(
|
||||
() => buttonResults[props.executionTrackingId],
|
||||
(newResult, oldResult) => {
|
||||
if (newResult) {
|
||||
renderExecutionResult({
|
||||
logEntry: newResult
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
// Expose methods for parent/imperative use
|
||||
defineExpose({
|
||||
reset,
|
||||
show,
|
||||
rerunAction,
|
||||
killAction,
|
||||
fetchExecutionResult,
|
||||
renderExecutionResult,
|
||||
hideEverythingApartFromOutput,
|
||||
handleClose
|
||||
})
|
||||
|
||||
</script>
|
||||
131
frontend/resources/vue/views/LoginView.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<Section title="Login to OliveTin" class="small">
|
||||
<div class="login-form" style="display: grid; grid-template-columns: max-content 1fr; gap: 1em;">
|
||||
<div v-if="!hasOAuth && !hasLocalLogin" class="login-disabled">
|
||||
<span>This server is not configured with either OAuth, or local users, so you cannot login.</span>
|
||||
</div>
|
||||
|
||||
<div v-if="hasOAuth" class="login-oauth2">
|
||||
<h3>OAuth Login</h3>
|
||||
<div class="oauth-providers">
|
||||
<button v-for="provider in oauthProviders" :key="provider.name" class="oauth-button"
|
||||
@click="loginWithOAuth(provider)">
|
||||
<span v-if="provider.icon" class="provider-icon" v-html="provider.icon"></span>
|
||||
<span class="provider-name">Login with {{ provider.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasLocalLogin" class="login-local">
|
||||
<h3>Local Login</h3>
|
||||
<form @submit.prevent="handleLocalLogin" class="local-login-form">
|
||||
<div v-if="loginError" class="error-message">
|
||||
{{ loginError }}
|
||||
</div>
|
||||
|
||||
<label for="username">Username:</label>
|
||||
<input id="username" v-model="username" type="text" name="username" autocomplete="username" required />
|
||||
|
||||
<label for="password">Password:</label>
|
||||
<input id="password" v-model="password" type="password" name="password" autocomplete="current-password"
|
||||
required />
|
||||
|
||||
<button type="submit" :disabled="loading" class="login-button">
|
||||
{{ loading ? 'Logging in...' : 'Login' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import Section from 'picocrank/vue/components/Section.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const loading = ref(false)
|
||||
const loginError = ref('')
|
||||
const hasOAuth = ref(false)
|
||||
const hasLocalLogin = ref(false)
|
||||
const oauthProviders = ref([])
|
||||
|
||||
async function fetchLoginOptions() {
|
||||
try {
|
||||
const response = await fetch('webUiSettings.json')
|
||||
const settings = await response.json()
|
||||
|
||||
hasOAuth.value = settings.AuthOAuth2Providers && settings.AuthOAuth2Providers.length > 0
|
||||
hasLocalLogin.value = settings.AuthLocalLogin
|
||||
|
||||
if (hasOAuth.value) {
|
||||
oauthProviders.value = settings.AuthOAuth2Providers
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch login options:', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLocalLogin() {
|
||||
loading.value = true
|
||||
loginError.value = ''
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: username.value,
|
||||
password: password.value
|
||||
})
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
// Redirect to home page on successful login
|
||||
router.push('/')
|
||||
} else {
|
||||
const error = await response.text()
|
||||
loginError.value = error || 'Login failed. Please check your credentials.'
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Login error:', err)
|
||||
loginError.value = 'Network error. Please try again.'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function loginWithOAuth(provider) {
|
||||
// Redirect to OAuth provider
|
||||
window.location.href = provider.authUrl
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchLoginOptions()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
section {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.login-view {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
form {
|
||||
grid-template-columns: max-content 1fr;
|
||||
gap: 1em;
|
||||
}
|
||||
</style>
|
||||
248
frontend/resources/vue/views/LogsListView.vue
Normal file
@@ -0,0 +1,248 @@
|
||||
<template>
|
||||
<Section title="Logs" :padding="false">
|
||||
<template #toolbar>
|
||||
<label class="input-with-icons">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||
<path fill="currentColor"
|
||||
d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14" />
|
||||
</svg>
|
||||
<input placeholder="Filter current page" v-model="searchText" />
|
||||
<button title="Clear search filter" :disabled="!searchText" @click="clearSearch">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||
<path fill="currentColor"
|
||||
d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z" />
|
||||
</svg>
|
||||
</button>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<p class = "padding">This is a list of logs from actions that have been executed. You can filter the list by action title.</p>
|
||||
<div v-show="filteredLogs.length > 0">
|
||||
<table class="logs-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Action</th>
|
||||
<th>Metadata</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="log in filteredLogs" :key="log.executionTrackingId" class="log-row" :title="log.actionTitle">
|
||||
<td class="timestamp">{{ formatTimestamp(log.datetimeStarted) }}</td>
|
||||
<td>
|
||||
<span class="icon" v-html="log.actionIcon"></span>
|
||||
<router-link :to="`/logs/${log.executionTrackingId}`">
|
||||
{{ log.actionTitle }}
|
||||
</router-link>
|
||||
</td>
|
||||
<td class="tags">
|
||||
<span class="annotation">
|
||||
<span class="annotation-key">User:</span>
|
||||
<span class="annotation-val">{{ log.user }}</span>
|
||||
</span>
|
||||
<span v-if="log.tags && log.tags.length > 0" class="tag-list">
|
||||
<span v-for="tag in log.tags" :key="tag" class="tag">{{ tag }}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="exit-code">
|
||||
<span :class="getStatusClass(log) + ' annotation'">
|
||||
{{ getStatusText(log) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Pagination :pageSize="pageSize" :total="totalCount" :currentPage="currentPage" @page-change="handlePageChange" class = "padding"
|
||||
@page-size-change="handlePageSizeChange" itemTitle="execution logs" />
|
||||
</div>
|
||||
|
||||
<div v-show="logs.length === 0" class="empty-state">
|
||||
<p>There are no logs to display.</p>
|
||||
<router-link to="/">Return to index</router-link>
|
||||
</div>
|
||||
</Section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import Pagination from '../components/Pagination.vue'
|
||||
import Section from 'picocrank/vue/components/Section.vue'
|
||||
|
||||
const logs = ref([])
|
||||
const searchText = ref('')
|
||||
const pageSize = ref(10)
|
||||
const currentPage = ref(1)
|
||||
const loading = ref(false)
|
||||
const totalCount = ref(0)
|
||||
|
||||
const filteredLogs = computed(() => {
|
||||
if (!searchText.value) {
|
||||
return logs.value
|
||||
}
|
||||
const searchLower = searchText.value.toLowerCase()
|
||||
return logs.value.filter(log =>
|
||||
log.actionTitle.toLowerCase().includes(searchLower)
|
||||
)
|
||||
})
|
||||
|
||||
async function fetchLogs() {
|
||||
loading.value = true
|
||||
try {
|
||||
const startOffset = (currentPage.value - 1) * pageSize.value
|
||||
|
||||
const args = {
|
||||
"startOffset": BigInt(startOffset),
|
||||
}
|
||||
|
||||
const response = await window.client.getLogs(args)
|
||||
|
||||
logs.value = response.logs
|
||||
pageSize.value = Number(response.pageSize) || 0
|
||||
totalCount.value = Number(response.totalCount) || 0
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch logs:', err)
|
||||
window.showBigError('fetch-logs', 'getting logs', err, false)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
searchText.value = ''
|
||||
}
|
||||
|
||||
function formatTimestamp(timestamp) {
|
||||
if (!timestamp) return 'Unknown'
|
||||
try {
|
||||
const date = new Date(timestamp)
|
||||
return date.toLocaleString()
|
||||
} catch (err) {
|
||||
return timestamp
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusClass(log) {
|
||||
if (log.timedOut) return 'status-timeout'
|
||||
if (log.blocked) return 'status-blocked'
|
||||
if (log.exitCode !== 0) return 'status-error'
|
||||
return 'status-success'
|
||||
}
|
||||
|
||||
function getStatusText(log) {
|
||||
if (log.timedOut) return 'Timed out'
|
||||
if (log.blocked) return 'Blocked'
|
||||
if (log.exitCode !== 0) return `Exit code ${log.exitCode}`
|
||||
return 'Completed'
|
||||
}
|
||||
|
||||
function handlePageChange(page) {
|
||||
currentPage.value = page
|
||||
fetchLogs()
|
||||
}
|
||||
|
||||
function handlePageSizeChange(newPageSize) {
|
||||
pageSize.value = newPageSize
|
||||
currentPage.value = 1 // Reset to first page
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchLogs()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.logs-view {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.input-with-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.input-with-icons input {
|
||||
border: none;
|
||||
outline: none;
|
||||
flex: 1;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.input-with-icons button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.input-with-icons button:hover:not(:disabled) {
|
||||
}
|
||||
|
||||
.input-with-icons button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-family: monospace;
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 0.5rem;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.content {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.content:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
color: #28a745;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
color: #dc3545;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-timeout {
|
||||
color: #ffc107;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-blocked {
|
||||
color: #6c757d;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.empty-state a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.empty-state a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
</style>
|
||||
60
frontend/resources/vue/views/NotFoundView.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="not-found-view">
|
||||
<div class="not-found-container">
|
||||
<div class="not-found-content">
|
||||
<h1>404</h1>
|
||||
<h2>Page Not Found</h2>
|
||||
|
||||
<div class="actions">
|
||||
<button class = "button good" @click="goToHome">
|
||||
Go to Home
|
||||
</button>
|
||||
<button class="button neutral" @click="goBack">
|
||||
Go Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'NotFoundView',
|
||||
methods: {
|
||||
goBack() {
|
||||
this.$router.go(-1)
|
||||
},
|
||||
goToHome() {
|
||||
this.$router.push('/')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.not-found-content {
|
||||
padding: 3rem 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.not-found-content h1 {
|
||||
font-size: 6rem;
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.not-found-content h2 {
|
||||
font-size: 2rem;
|
||||
margin: 0 0 1rem 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.not-found-content p {
|
||||
font-size: 1.1rem;
|
||||
color: #666;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
</style>
|
||||
73
frontend/style.css
Normal file
@@ -0,0 +1,73 @@
|
||||
header {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
aside {
|
||||
padding-top: 4em;
|
||||
z-index: 3; /* Make sure the sidebar is on top of the terminal */
|
||||
}
|
||||
|
||||
fieldset {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, 180px);
|
||||
grid-auto-rows: 1fr;
|
||||
justify-content: center;
|
||||
place-items: stretch;
|
||||
}
|
||||
|
||||
main {
|
||||
padding-top: 4em;
|
||||
}
|
||||
|
||||
dialog {
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
legend {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 1em;
|
||||
padding-top: 1.5em;
|
||||
}
|
||||
|
||||
button.neutral {
|
||||
background-color: transparent;
|
||||
color: white;
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.display {
|
||||
border: 1px solid #666;
|
||||
padding: 1em;
|
||||
border-radius: .7em;
|
||||
box-shadow: 0 0 .6em #aaa;
|
||||
text-align: center;
|
||||
font-size: small;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
aside .flex-row {
|
||||
padding-left: 1em;
|
||||
padding-right: .5em;
|
||||
}
|
||||
|
||||
#sidebar-toggler-button {
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
div.buttons button svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
section.small {
|
||||
border-radius: .4em;
|
||||
}
|
||||
29
frontend/vite.config.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
Components({
|
||||
dirs: ['resources/vue/'],
|
||||
extensions: ['vue'],
|
||||
deep: true,
|
||||
dts: false,
|
||||
}),
|
||||
vue(),
|
||||
],
|
||||
server: {
|
||||
proxy: {
|
||||
'/webUiSettings.json': {
|
||||
target: 'http://localhost:1337',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
'/api': {
|
||||
target: 'http://localhost:1337',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
133
go.mod
@@ -1,133 +0,0 @@
|
||||
module github.com/OliveTin/OliveTin
|
||||
|
||||
go 1.21
|
||||
|
||||
toolchain go1.21.9
|
||||
|
||||
require (
|
||||
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.11.1
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
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.3
|
||||
github.com/spf13/viper v1.15.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.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/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/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.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.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/uuid/v5 v5.0.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // 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/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/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 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 // 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/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.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.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
|
||||
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
|
||||
)
|
||||
745
go.sum
@@ -1,745 +0,0 @@
|
||||
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=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
|
||||
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
|
||||
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
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/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/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/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/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=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
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/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.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.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=
|
||||
github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU=
|
||||
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/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=
|
||||
github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA=
|
||||
github.com/go-toolsmith/pkgload v1.2.2 h1:0CtmHq/02QhxcF7E9N5LIFcYFsMR5rdovfqTtRKkgIk=
|
||||
github.com/go-toolsmith/pkgload v1.2.2/go.mod h1:R2hxLNRKuAsiXCo2i5J6ZQPhnPMOVtU+f0arbFPWCus=
|
||||
github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
|
||||
github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw=
|
||||
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=
|
||||
github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
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/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=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
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.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=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
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.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=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
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-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.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/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=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
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/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/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.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/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/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 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-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=
|
||||
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
|
||||
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/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=
|
||||
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0=
|
||||
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs=
|
||||
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ=
|
||||
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.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.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.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=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
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.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.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=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
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/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=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
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.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=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
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-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-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=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
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.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=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
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.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=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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.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=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
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-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.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.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.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.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=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
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.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=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
||||
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
|
||||
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
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/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=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
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.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=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
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.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-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=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
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=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
@@ -4,6 +4,7 @@ test-install:
|
||||
npm install --no-fund
|
||||
|
||||
test-run:
|
||||
# GitHub Actions fails badly on the default timeout of 2000ms
|
||||
npx mocha -t 10000
|
||||
|
||||
find-flakey-tests:
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
logLevel: debug
|
||||
|
||||
actions:
|
||||
- title: Ping
|
||||
shell: echo "Ping executed"
|
||||
icon: ping
|
||||
|
||||
- title: Action 1
|
||||
shell: echo "Action 1 executed"
|
||||
icon: check
|
||||
- title: Action 2
|
||||
shell: echo "Action 2 executed"
|
||||
icon: check
|
||||
|
||||
- title: Action 3
|
||||
shell: echo "Action 3 executed"
|
||||
icon: check
|
||||
- title: Action 4
|
||||
shell: echo "Action 4 executed"
|
||||
icon: check
|
||||
|
||||
dashboards:
|
||||
- title: Test
|
||||
contents:
|
||||
# Uncomment to see the dashboard with the "Ping" action only
|
||||
- title: Ping
|
||||
- title: Fieldset 1
|
||||
type: fieldset
|
||||
contents:
|
||||
- title: Action 1
|
||||
- title: Action 2
|
||||
- title: Fieldset 2
|
||||
type: fieldset
|
||||
contents:
|
||||
- title: Action 3
|
||||
- title: Action 4
|
||||
@@ -0,0 +1,21 @@
|
||||
#
|
||||
# Integration Test Config: emptyDashboardsAreHidden
|
||||
#
|
||||
|
||||
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||
|
||||
logLevel: "DEBUG"
|
||||
checkForUpdates: false
|
||||
|
||||
actions:
|
||||
- title: Ping
|
||||
shell: ping example.com
|
||||
icon: ping
|
||||
entity: server
|
||||
|
||||
|
||||
|
||||
dashboards:
|
||||
- title: Empty Dashboard
|
||||
contents: []
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
actions:
|
||||
- title: 'Test me {{ test_me.name }}'
|
||||
popupOnStart: execution-dialog-stdout-only
|
||||
entity: testrows
|
||||
shell: echo "{{ test_me.val }}"
|
||||
|
||||
entities:
|
||||
- name: testrows
|
||||
file: entities/data.json
|
||||
@@ -0,0 +1,5 @@
|
||||
{"name":"INT with 10 numbers","val":1234567890}
|
||||
{"name":"INT with 6 numbers","val":123456}
|
||||
{"name":"INT with 7 numbers","val":1234567}
|
||||
{"name":"FLOAT with 6 numbers","val":1.234567}
|
||||
{"name":"FLOAT with 10 numbers","val":1.234567890}
|
||||
14
integration-tests/configs/policy-all-false/config.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
# Integration Test Config: Policy All False
|
||||
#
|
||||
|
||||
logLevel: "DEBUG"
|
||||
checkForUpdates: false
|
||||
|
||||
defaultPolicy:
|
||||
showDiagnostics: false
|
||||
showLogList: false
|
||||
|
||||
actions:
|
||||
- title: sleep 2 seconds
|
||||
shell: sleep 2
|
||||
icon: "🥱"
|
||||
@@ -3,23 +3,62 @@ 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 async function getActionButtons (dashboardTitle = null) {
|
||||
// New Vue UI renders action buttons using ActionButton.vue structure
|
||||
// Each button lives under a container with class .action-button
|
||||
if (dashboardTitle == null) {
|
||||
return await webdriver.findElements(By.css('.action-button button'))
|
||||
} else {
|
||||
return await webdriver.findElements(By.css('section[title="' + dashboardTitle + '"] .action-button button'))
|
||||
}
|
||||
}
|
||||
|
||||
export function takeScreenshot (webdriver) {
|
||||
export async function getExecutionDialogOutput() {
|
||||
await webdriver.wait(new Condition('Dialog with long int is visible', async () => {
|
||||
const dialog = await webdriver.findElement({ id: 'execution-results-popup' })
|
||||
return await dialog.isDisplayed()
|
||||
}));
|
||||
|
||||
const ret = await webdriver.executeScript('return window.logEntries.get(window.executionDialog.executionTrackingId).output')
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
export async function closeExecutionDialog() {
|
||||
const btnClose = await webdriver.findElements(By.css('[title="Close"]'))
|
||||
await btnClose[0].click()
|
||||
}
|
||||
|
||||
export function takeScreenshotOnFailure (test, webdriver) {
|
||||
if (test.state === 'failed') {
|
||||
const title = test.fullTitle();
|
||||
|
||||
console.log(`Test failed, taking screenshot: ${title}`);
|
||||
takeScreenshot(webdriver, title);
|
||||
}
|
||||
}
|
||||
|
||||
export function takeScreenshot (webdriver, title) {
|
||||
return webdriver.takeScreenshot().then((img) => {
|
||||
fs.writeFileSync('out.png', img, 'base64')
|
||||
fs.mkdirSync('screenshots', { recursive: true });
|
||||
|
||||
title = title.replaceAll('config: ', '')
|
||||
title = title.replaceAll(/[\(\)\|\*\<\>\:]/g, "_")
|
||||
title = title + '.failed-test'
|
||||
|
||||
fs.writeFileSync('screenshots/' + title + '.png', img, 'base64')
|
||||
})
|
||||
}
|
||||
|
||||
export async function getRootAndWait() {
|
||||
await webdriver.get(runner.baseUrl())
|
||||
await webdriver.wait(new Condition('wait for initial-marshal-complete', async function() {
|
||||
await webdriver.wait(new Condition('wait for loaded-dashboard', async function() {
|
||||
const body = await webdriver.findElement(By.tagName('body'))
|
||||
const attr = await body.getAttribute('initial-marshal-complete')
|
||||
const attr = await body.getAttribute('loaded-dashboard')
|
||||
|
||||
if (attr == 'true') {
|
||||
console.log('loaded-dashboard: ', attr)
|
||||
|
||||
if (attr) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@@ -27,18 +66,65 @@ export async function getRootAndWait() {
|
||||
}))
|
||||
}
|
||||
|
||||
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')
|
||||
export async function closeSidebar() {
|
||||
await webdriver.findElement(By.id('sidebar-toggler-button')).click()
|
||||
|
||||
const sidebar = await webdriver.findElement(By.id('mainnav'))
|
||||
|
||||
const neededLeft = '-250px' // Assuming sidebar is closed at this position
|
||||
|
||||
let lastLeft = ''
|
||||
|
||||
await webdriver.wait(new Condition('wait for sidebar to close', async function() {
|
||||
const left = await sidebar.getCssValue('left')
|
||||
|
||||
if (left !== lastLeft) {
|
||||
lastLeft = left
|
||||
console.log('Sidebar left changed to: ', left)
|
||||
return false
|
||||
} else {
|
||||
console.log('Sidebar closed, left is: *' + left, left === neededLeft ? ' (as expected)' : '')
|
||||
return left === neededLeft
|
||||
}
|
||||
}), 10000); // Wait up to 10 seconds for the sidebar to close
|
||||
}
|
||||
|
||||
export async function openSidebar() {
|
||||
await webdriver.findElement(By.id('sidebar-toggler-button')).click()
|
||||
|
||||
const sidebar = await webdriver.findElement(By.id('mainnav'))
|
||||
|
||||
let lastLeft = 0
|
||||
|
||||
await webdriver.wait(new Condition('wait for sidebar to open', async function() {
|
||||
const left = await sidebar.getCssValue('left')
|
||||
|
||||
if (left !== lastLeft) {
|
||||
lastLeft = left
|
||||
console.log('Sidebar left changed to: ', left)
|
||||
return false
|
||||
} else {
|
||||
console.log('Sidebar opened, left is: ', left)
|
||||
return true
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getNavigationLinks() {
|
||||
const navigationLinks = await webdriver.findElements(By.css('.navigation-links li'))
|
||||
|
||||
return navigationLinks
|
||||
}
|
||||
|
||||
export async function requireExecutionDialogStatus (webdriver, expected) {
|
||||
await webdriver.wait(new Condition('wait for action to be running', async function () {
|
||||
const actual = await webdriver.executeScript('return window.executionDialog.domStatus.getText()')
|
||||
const dialogStatus = await webdriver.findElement(By.id('execution-dialog-status'))
|
||||
const actual = await dialogStatus.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
|
||||
}
|
||||
}))
|
||||
|
||||
1846
integration-tests/package-lock.json
generated
@@ -11,12 +11,12 @@
|
||||
"author": "",
|
||||
"license": "AGPL-3.0-only",
|
||||
"devDependencies": {
|
||||
"chai": "^5.1.0",
|
||||
"eslint": "^8.51.0",
|
||||
"mocha": "^10.4.0",
|
||||
"selenium-webdriver": "^4.19.0"
|
||||
"chai": "^6.2.0",
|
||||
"eslint": "^9.37.0",
|
||||
"mocha": "^11.7.4",
|
||||
"selenium-webdriver": "^4.36.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"wait-on": "^7.2.0"
|
||||
"wait-on": "^9.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,9 @@ class OliveTinTestRunnerStartLocalProcess extends OliveTinTestRunner {
|
||||
let stdout = ""
|
||||
let stderr = ""
|
||||
|
||||
this.ot = spawn('./../OliveTin', ['-configdir', 'configs/' + cfg + '/'])
|
||||
console.log(" OliveTin starting local process...")
|
||||
|
||||
this.ot = spawn('./../service/OliveTin', ['-configdir', 'configs/' + cfg + '/'])
|
||||
|
||||
let logStdout = false
|
||||
|
||||
@@ -74,6 +76,8 @@ class OliveTinTestRunnerStartLocalProcess extends OliveTinTestRunner {
|
||||
if (this.ot.exitCode == null) {
|
||||
this.BASE_URL = 'http://localhost:1337/'
|
||||
|
||||
console.log(" OliveTin waiting for local process to start...")
|
||||
|
||||
await waitOn({
|
||||
resources: [this.BASE_URL]
|
||||
})
|
||||
@@ -95,7 +99,12 @@ class OliveTinTestRunnerStartLocalProcess extends OliveTinTestRunner {
|
||||
console.log(" OliveTin local process killed")
|
||||
}
|
||||
|
||||
await new Promise((res) => setTimeout(res, 100))
|
||||
if (process.env.CI === 'true') {
|
||||
// GitHub runners seem to need a bit more time to clean up
|
||||
await new Promise((res) => setTimeout(res, 3000))
|
||||
} else {
|
||||
await new Promise((res) => setTimeout(res, 100))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
63
integration-tests/test/dashboardsWithBasicFieldsets.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import { describe, it, before, after } from 'mocha'
|
||||
import { expect, assert } from 'chai'
|
||||
import { By, until, Condition } from 'selenium-webdriver'
|
||||
//import * as waitOn from 'wait-on'
|
||||
import {
|
||||
getRootAndWait,
|
||||
getActionButtons,
|
||||
openSidebar,
|
||||
getNavigationLinks,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../lib/elements.js'
|
||||
|
||||
describe('config: dashboards with basic fieldsets', function () {
|
||||
before(async function () {
|
||||
await runner.start('dashboardsWithBasicFieldsets')
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
takeScreenshotOnFailure(this.currentTest, webdriver);
|
||||
});
|
||||
|
||||
it('Dashboards with basic fieldsets', async function () {
|
||||
await getRootAndWait()
|
||||
|
||||
const title = await webdriver.getTitle()
|
||||
expect(title).to.be.equal("Test - OliveTin")
|
||||
|
||||
await openSidebar()
|
||||
|
||||
const navigationLinks = await getNavigationLinks()
|
||||
assert.equal(navigationLinks.length, 5, 'Expected the nav to only have 5 links') // test dashboard + logs + diagnostics + entities + separator
|
||||
|
||||
const firstLink = await navigationLinks[0]
|
||||
|
||||
expect(await firstLink.getAttribute('title')).to.be.equal('Test', 'Expected the first link to be the actions link')
|
||||
|
||||
const actionButtons = await getActionButtons()
|
||||
expect(actionButtons).to.have.length(5, 'Expected 5 action buttons')
|
||||
|
||||
// Check that we have the expected number of fieldsets
|
||||
const allFieldsets = await webdriver.findElements(By.css('fieldset'))
|
||||
expect(allFieldsets).to.have.length(5, 'Expected 5 fieldsets total')
|
||||
|
||||
// Check that we have fieldsets with the expected titles
|
||||
const fieldsetTitles = []
|
||||
for (let i = 0; i < allFieldsets.length; i++) {
|
||||
const legend = await allFieldsets[i].findElements(By.css('legend'))
|
||||
if (legend.length > 0) {
|
||||
const title = await legend[0].getText()
|
||||
fieldsetTitles.push(title)
|
||||
}
|
||||
}
|
||||
|
||||
// We should have fieldsets for: Fieldset 1, Fieldset 2, and Actions fieldsets
|
||||
expect(fieldsetTitles).to.include('Fieldset 1')
|
||||
expect(fieldsetTitles).to.include('Fieldset 2')
|
||||
|
||||
})
|
||||
})
|
||||
37
integration-tests/test/emptyDashboardsAreHidden.js
Normal file
@@ -0,0 +1,37 @@
|
||||
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,
|
||||
openSidebar,
|
||||
getNavigationLinks,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../lib/elements.js'
|
||||
|
||||
describe('config: empty dashboards are hidden', function () {
|
||||
before(async function () {
|
||||
await runner.start('emptyDashboardsAreHidden')
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
takeScreenshotOnFailure(this.currentTest, webdriver);
|
||||
});
|
||||
|
||||
it('Test hidden dashboard', async function () {
|
||||
await getRootAndWait()
|
||||
|
||||
await openSidebar()
|
||||
|
||||
const title = await webdriver.getTitle()
|
||||
expect(title).to.be.equal("Actions - OliveTin")
|
||||
|
||||
const navigationLinks = await getNavigationLinks()
|
||||
expect(navigationLinks).to.not.be.empty
|
||||
expect(navigationLinks.length).to.be.equal(4, 'Expected the nav to only have 4 links')
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,11 @@
|
||||
import { describe, it, before, after } from 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import { By, until } from 'selenium-webdriver'
|
||||
import { getRootAndWait, takeScreenshot } from '../lib/elements.js'
|
||||
import {
|
||||
getRootAndWait,
|
||||
takeScreenshot,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../lib/elements.js'
|
||||
|
||||
describe('config: entities', function () {
|
||||
before(async function () {
|
||||
@@ -12,19 +16,27 @@ describe('config: entities', function () {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
takeScreenshotOnFailure(this.currentTest, webdriver);
|
||||
});
|
||||
|
||||
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)
|
||||
// The old test was looking for #root-group, but that doesn't exist in the new Vue UI
|
||||
// Instead, we should look for action buttons directly
|
||||
const actionButtons = await webdriver.findElements(By.css('.action-button button'))
|
||||
expect(actionButtons).to.not.be.null
|
||||
expect(actionButtons).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')
|
||||
expect(await actionButtons[0].getAttribute('title')).to.be.equal('Ping server1')
|
||||
expect(await actionButtons[1].getAttribute('title')).to.be.equal('Ping server2')
|
||||
expect(await actionButtons[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
|
||||
// Check that there's no error dialog visible
|
||||
const dialogErr = await webdriver.findElements(By.id('big-error'))
|
||||
if (dialogErr.length > 0) {
|
||||
expect(await dialogErr[0].isDisplayed()).to.be.false
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// Issue: https://github.com/OliveTin/OliveTin/issues/616
|
||||
import { describe, it, before, after } from 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import { By, until, Condition } from 'selenium-webdriver'
|
||||
import {
|
||||
getRootAndWait,
|
||||
getActionButtons,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../lib/elements.js'
|
||||
|
||||
describe('config: entities', function () {
|
||||
before(async function () {
|
||||
await runner.start('entityFilesWithLongIntsUseStandardForm')
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
takeScreenshotOnFailure(this.currentTest, webdriver);
|
||||
});
|
||||
|
||||
it('Entity buttons are rendered', async function() {
|
||||
await getRootAndWait()
|
||||
|
||||
const buttons = await getActionButtons()
|
||||
|
||||
expect(buttons).to.not.be.null
|
||||
expect(buttons).to.have.length(5)
|
||||
|
||||
// Test INT with 10 numbers
|
||||
const buttonInt10 = await buttons[2]
|
||||
expect(await buttonInt10.getAttribute('title')).to.be.equal('Test me INT with 10 numbers')
|
||||
await buttonInt10.click()
|
||||
|
||||
// Wait for navigation to execution view
|
||||
await webdriver.wait(new Condition('wait for execution view', async () => {
|
||||
const url = await webdriver.getCurrentUrl()
|
||||
return url.includes('/logs/') && !url.endsWith('/logs')
|
||||
}), 10000)
|
||||
|
||||
// Wait for execution to complete - look for the execution status
|
||||
await webdriver.wait(new Condition('wait for execution status', async () => {
|
||||
const statusElement = await webdriver.findElements(By.id('execution-dialog-status'))
|
||||
return statusElement.length > 0
|
||||
}), 15000)
|
||||
|
||||
// Check that the execution completed successfully by looking at the status
|
||||
const statusElement = await webdriver.findElement(By.id('execution-dialog-status'))
|
||||
const statusText = await statusElement.getText()
|
||||
|
||||
// The status should indicate success (not "Executing..." or "Failed")
|
||||
expect(statusText).to.not.include('Executing')
|
||||
expect(statusText).to.not.include('Failed')
|
||||
|
||||
// Verify that we're on the execution page by checking the URL
|
||||
const currentUrl = await webdriver.getCurrentUrl()
|
||||
expect(currentUrl).to.include('/logs/')
|
||||
expect(currentUrl).to.not.equal(runner.baseUrl() + '/logs')
|
||||
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,12 @@ 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'
|
||||
import {
|
||||
getRootAndWait,
|
||||
getActionButtons,
|
||||
takeScreenshotOnFailure,
|
||||
openSidebar,
|
||||
} from '../lib/elements.js'
|
||||
|
||||
describe('config: general', function () {
|
||||
before(async function () {
|
||||
@@ -13,23 +18,29 @@ describe('config: general', function () {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
takeScreenshotOnFailure(this.currentTest, webdriver);
|
||||
});
|
||||
|
||||
it('Page title', async function () {
|
||||
await webdriver.get(runner.baseUrl())
|
||||
|
||||
const title = await webdriver.getTitle()
|
||||
expect(title).to.be.equal("OliveTin")
|
||||
expect(title).to.be.equal("Actions - OliveTin")
|
||||
})
|
||||
|
||||
it('Page title2', async function () {
|
||||
/*
|
||||
await webdriver.get(runner.baseUrl())
|
||||
it('navbar contains default policy links', async function () {
|
||||
await getRootAndWait()
|
||||
await openSidebar()
|
||||
|
||||
const title = await webdriver.getTitle()
|
||||
expect(title).to.be.equal("OliveTin")
|
||||
*/
|
||||
|
||||
const logsLink = await webdriver.findElements(By.css('a[href="/logs"]'))
|
||||
const diagnosticsLink = await webdriver.findElements(By.css('a[href="/diagnostics"]'))
|
||||
|
||||
expect(logsLink).to.not.be.empty
|
||||
expect(diagnosticsLink).to.not.be.empty
|
||||
})
|
||||
|
||||
|
||||
it('Footer contains promo', async function () {
|
||||
const ftr = await webdriver.findElement(By.tagName('footer')).getText()
|
||||
|
||||
@@ -39,14 +50,23 @@ describe('config: general', function () {
|
||||
it('Default buttons are rendered', async function() {
|
||||
await getRootAndWait()
|
||||
|
||||
const buttons = await getActionButtons(webdriver)
|
||||
await webdriver.wait(new Condition('wait for action buttons', async () => {
|
||||
const btns = await webdriver.findElements(By.css('[title="dir-popup"], [title="cd-passive"], .action-button button'))
|
||||
return btns.length >= 1
|
||||
}), 10000)
|
||||
|
||||
expect(buttons).to.have.length(8)
|
||||
const buttons = await getActionButtons()
|
||||
expect(buttons.length).to.be.greaterThanOrEqual(4)
|
||||
})
|
||||
|
||||
it('Start dir action (popup)', async function () {
|
||||
await getRootAndWait()
|
||||
|
||||
await webdriver.wait(new Condition('wait for dir-popup button', async () => {
|
||||
const btns = await webdriver.findElements(By.css('[title="dir-popup"]'))
|
||||
return btns.length === 1
|
||||
}), 10000)
|
||||
|
||||
const buttons = await webdriver.findElements(By.css('[title="dir-popup"]'))
|
||||
|
||||
expect(buttons).to.have.length(1)
|
||||
@@ -57,20 +77,21 @@ describe('config: general', function () {
|
||||
|
||||
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
|
||||
// New UI navigates to /logs/<id> instead of showing old dialog
|
||||
await webdriver.wait(new Condition('wait navigate to logs', async () => {
|
||||
const url = await webdriver.getCurrentUrl()
|
||||
return url.includes('/logs/')
|
||||
}), 8000)
|
||||
})
|
||||
|
||||
it('Start cd action (passive)', async function () {
|
||||
await getRootAndWait()
|
||||
|
||||
await webdriver.wait(new Condition('wait for cd-passive button', async () => {
|
||||
const btns = await webdriver.findElements(By.css('[title="cd-passive"]'))
|
||||
return btns.length === 1
|
||||
}), 10000)
|
||||
|
||||
const buttons = await webdriver.findElements(By.css('[title="cd-passive"]'))
|
||||
|
||||
expect(buttons).to.have.length(1)
|
||||
@@ -81,15 +102,10 @@ describe('config: general', function () {
|
||||
|
||||
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
|
||||
// Should not navigate to logs for passive action
|
||||
await webdriver.sleep(500)
|
||||
const url = await webdriver.getCurrentUrl()
|
||||
expect(url.includes('/logs/')).to.be.false
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@@ -2,6 +2,11 @@ import { describe, it, before, after } from 'mocha'
|
||||
import { expect } from 'chai'
|
||||
|
||||
import { By } from 'selenium-webdriver'
|
||||
import {
|
||||
getRootAndWait,
|
||||
getActionButtons,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../lib/elements.js'
|
||||
|
||||
describe('config: hiddenFooter', function () {
|
||||
before(async function () {
|
||||
@@ -12,11 +17,15 @@ describe('config: hiddenFooter', function () {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
takeScreenshotOnFailure(this.currentTest, webdriver);
|
||||
});
|
||||
|
||||
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
|
||||
// Pass when footer element is not found, fail if it exists
|
||||
const footers = await webdriver.findElements(By.tagName('footer'))
|
||||
expect(footers.length).to.equal(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { expect } from 'chai'
|
||||
import { By } from 'selenium-webdriver'
|
||||
import {
|
||||
getRootAndWait,
|
||||
getActionButtons,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../lib/elements.js'
|
||||
|
||||
|
||||
describe('config: hiddenNav', function () {
|
||||
before(async function () {
|
||||
@@ -10,11 +16,15 @@ describe('config: hiddenNav', function () {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
takeScreenshotOnFailure(this.currentTest, webdriver);
|
||||
});
|
||||
|
||||
it('nav is hidden', async () => {
|
||||
await webdriver.get(runner.baseUrl())
|
||||
await getRootAndWait()
|
||||
|
||||
const toggler = await webdriver.findElement(By.tagName('header'))
|
||||
//const toggler = await webdriver.findElements(By.id('sidebar-toggler-button'))
|
||||
|
||||
expect(await toggler.isDisplayed()).to.be.false
|
||||
//expect(toggler).to.be.empty
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { describe, it, before, after } from 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import { By, until } from 'selenium-webdriver'
|
||||
import { getActionButtons, getRootAndWait } from '../lib/elements.js'
|
||||
import { By, until, Condition } from 'selenium-webdriver'
|
||||
import {
|
||||
getRootAndWait,
|
||||
getActionButtons,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../lib/elements.js'
|
||||
|
||||
|
||||
describe('config: multipleDropdowns', function () {
|
||||
before(async function () {
|
||||
@@ -12,10 +17,20 @@ describe('config: multipleDropdowns', function () {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
takeScreenshotOnFailure(this.currentTest, webdriver);
|
||||
});
|
||||
|
||||
it('Multiple dropdowns are possible', async function() {
|
||||
await getRootAndWait()
|
||||
|
||||
const buttons = await getActionButtons(webdriver)
|
||||
// Wait for action buttons to be rendered
|
||||
await webdriver.wait(new Condition('wait for action buttons', async () => {
|
||||
const btns = await webdriver.findElements(By.css('.action-button button'))
|
||||
return btns.length >= 2
|
||||
}), 10000)
|
||||
|
||||
const buttons = await getActionButtons()
|
||||
|
||||
let button = null
|
||||
for (const b of buttons) {
|
||||
@@ -31,11 +46,20 @@ describe('config: multipleDropdowns', function () {
|
||||
|
||||
await button.click()
|
||||
|
||||
const dialog = await webdriver.findElement(By.id('argument-popup'))
|
||||
// Wait for navigation to argument form page
|
||||
await webdriver.wait(new Condition('wait for argument form page', async () => {
|
||||
const url = await webdriver.getCurrentUrl()
|
||||
return url.includes('/actionBinding/') && url.includes('/argumentForm')
|
||||
}), 8000)
|
||||
|
||||
await webdriver.wait(until.elementIsVisible(dialog), 3500)
|
||||
// Wait for form elements to be rendered
|
||||
await webdriver.wait(new Condition('wait for form elements', async () => {
|
||||
const selects = await webdriver.findElements(By.tagName('select'))
|
||||
return selects.length >= 2
|
||||
}), 5000)
|
||||
|
||||
const selects = await dialog.findElements(By.tagName('select'))
|
||||
// Find the select elements after the wait condition
|
||||
const selects = await webdriver.findElements(By.tagName('select'))
|
||||
|
||||
expect(selects).to.have.length(2)
|
||||
expect(await selects[0].findElements(By.tagName('option'))).to.have.length(2)
|
||||
|
||||
51
integration-tests/test/onlyDashboards.mjs
Normal file
@@ -0,0 +1,51 @@
|
||||
import { describe, it, before, after } from 'mocha'
|
||||
import { assert, expect } from 'chai'
|
||||
import { By } from 'selenium-webdriver'
|
||||
import {
|
||||
getRootAndWait,
|
||||
getActionButtons,
|
||||
getNavigationLinks,
|
||||
openSidebar,
|
||||
closeSidebar,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../lib/elements.js'
|
||||
|
||||
describe('config: onlyDashboards', function () {
|
||||
before(async function () {
|
||||
await runner.start('onlyDashboards')
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
takeScreenshotOnFailure(this.currentTest, webdriver);
|
||||
});
|
||||
|
||||
it('When there are only dashboards, actions are hidden', async function () {
|
||||
await getRootAndWait()
|
||||
|
||||
await openSidebar()
|
||||
|
||||
const navLinks = await getNavigationLinks()
|
||||
expect(navLinks).to.not.be.empty
|
||||
|
||||
for (const link of navLinks) {
|
||||
console.log(await link.getAttribute('title'))
|
||||
}
|
||||
|
||||
const firstLink = await navLinks[0];
|
||||
assert.isNotNull(firstLink, 'Actions link should not be null')
|
||||
|
||||
assert.equal(await firstLink.getAttribute('title'), 'My Dashboard', 'First link should have the title "My Dashboard"')
|
||||
|
||||
const firstDashboardLink = await webdriver.findElement(By.css('li[title="My Dashboard"]'), 'The first dashboard link should be present')
|
||||
assert.isNotNull(firstDashboardLink, 'First dashboard link should not be null')
|
||||
assert.isTrue(await firstDashboardLink.isDisplayed(), 'First dashboard link should be displayed')
|
||||
|
||||
const actionButtonsOnDashboard = await getActionButtons()
|
||||
assert.isArray(actionButtonsOnDashboard, 'Action buttons on dashboard should be an array')
|
||||
assert.lengthOf(actionButtonsOnDashboard, 3, 'Action buttons on dashboard should have 3 buttons')
|
||||
})
|
||||
})
|
||||
32
integration-tests/test/policy-all-false.mjs
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
getRootAndWait,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../lib/elements.js'
|
||||
|
||||
import { By } from 'selenium-webdriver'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('config: policy-all-false', function () {
|
||||
before(async function () {
|
||||
await runner.start('policy-all-false')
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await runner.stop()
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
takeScreenshotOnFailure(this.currentTest, webdriver);
|
||||
});
|
||||
|
||||
|
||||
it('navbar should not contain default policy links', async function () {
|
||||
await getRootAndWait()
|
||||
|
||||
const logListLink = await webdriver.findElements(By.css('[href="/logs"]'))
|
||||
expect(logListLink).to.be.empty
|
||||
|
||||
const diagnosticsLink = await webdriver.findElements(By.css('[href="/diagnostics"]'))
|
||||
expect(diagnosticsLink).to.be.empty
|
||||
})
|
||||
})
|
||||
@@ -2,12 +2,14 @@ import { describe, it, before, after } from 'mocha'
|
||||
import { expect } from 'chai'
|
||||
|
||||
import { By } from 'selenium-webdriver'
|
||||
import {
|
||||
takeScreenshotOnFailure,
|
||||
} from '../lib/elements.js'
|
||||
|
||||
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 () {
|
||||
@@ -19,8 +21,12 @@ describe('config: prometheus', function () {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
takeScreenshotOnFailure(this.currentTest, webdriver);
|
||||
});
|
||||
|
||||
it('Metrics are available with correct types', async () => {
|
||||
webdriver.get(runner.metricsUrl())
|
||||
await webdriver.get(runner.metricsUrl())
|
||||
const prometheusOutput = await webdriver.findElement(By.tagName('pre')).getText()
|
||||
|
||||
expect(prometheusOutput).to.not.be.null
|
||||
|
||||
@@ -4,6 +4,7 @@ import { expect } from 'chai'
|
||||
import { By, Condition } from 'selenium-webdriver'
|
||||
import {
|
||||
takeScreenshot,
|
||||
takeScreenshotOnFailure,
|
||||
findExecutionDialog,
|
||||
requireExecutionDialogStatus,
|
||||
getRootAndWait,
|
||||
@@ -19,30 +20,30 @@ describe('config: sleep', function () {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
takeScreenshotOnFailure(this.currentTest, webdriver);
|
||||
});
|
||||
|
||||
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()
|
||||
|
||||
await webdriver.sleep(1000)
|
||||
|
||||
const dialog = await findExecutionDialog(webdriver)
|
||||
|
||||
expect(await dialog.isDisplayed()).to.be.true
|
||||
|
||||
await requireExecutionDialogStatus(webdriver, "unknown")
|
||||
await requireExecutionDialogStatus(webdriver, "Still running...")
|
||||
|
||||
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")
|
||||
}
|
||||
await requireExecutionDialogStatus(webdriver, "Completed Exit code: -1")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { expect } from 'chai'
|
||||
import {
|
||||
getRootAndWait,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../lib/elements.js'
|
||||
|
||||
describe('config: trustedHeader', function () {
|
||||
before(async function () {
|
||||
@@ -9,18 +13,32 @@ describe('config: trustedHeader', function () {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
it('req with X-User', async () => {
|
||||
const req = await fetch(runner.baseUrl() + '/api/WhoAmI', {
|
||||
afterEach(function () {
|
||||
takeScreenshotOnFailure(this.currentTest, webdriver);
|
||||
});
|
||||
|
||||
it.skip('req with X-User', async () => {
|
||||
await getRootAndWait()
|
||||
|
||||
// Use the Connect RPC client format
|
||||
const req = await fetch(runner.baseUrl() + '/api/Init', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"X-User": "fred",
|
||||
}
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
|
||||
console.log(`Final URL: ${req.url}, Status: ${req.status}`)
|
||||
|
||||
if (!req.ok) {
|
||||
console.log(req)
|
||||
console.log('Request failed:', req.status, req.statusText)
|
||||
const text = await req.text()
|
||||
console.log('Response body:', text)
|
||||
}
|
||||
|
||||
expect(req.ok, 'WhoAmI Request is ' + req.status).to.be.true
|
||||
expect(req.ok, 'Init Request is ' + req.status).to.be.true
|
||||
|
||||
const json = await req.json()
|
||||
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"context"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
"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
|
||||
Usergroup string
|
||||
|
||||
acls []string
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
logAclNoneMatched(cfg, aclFunction, user, action, cfg.DefaultPermissions.Logs)
|
||||
|
||||
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 {
|
||||
if action.Hidden {
|
||||
return false
|
||||
}
|
||||
|
||||
return aclCheck(View, cfg.DefaultPermissions.View, cfg, "isAllowedView", user, action)
|
||||
}
|
||||
|
||||
func getMetadataKeyOrEmpty(md metadata.MD, key string) string {
|
||||
mdValues := md.Get(key)
|
||||
|
||||
if len(mdValues) > 0 {
|
||||
return mdValues[0]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// UserFromContext tries to find a user from a grpc context
|
||||
func UserFromContext(ctx context.Context, cfg *config.Config) *AuthenticatedUser {
|
||||
ret := &AuthenticatedUser{}
|
||||
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
|
||||
if ok {
|
||||
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)
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"username": ret.Username,
|
||||
"usergroup": ret.Usergroup,
|
||||
}).Debugf("UserFromContext")
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func UserFromSystem(cfg *config.Config, username string) *AuthenticatedUser {
|
||||
ret := &AuthenticatedUser{
|
||||
Username: username,
|
||||
Usergroup: "system",
|
||||
}
|
||||
|
||||
buildUserAcls(cfg, ret)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func buildUserAcls(cfg *config.Config, user *AuthenticatedUser) {
|
||||
for _, acl := range cfg.AccessControlLists {
|
||||
if slices.Contains(acl.MatchUsernames, user.Username) {
|
||||
user.acls = append(user.acls, acl.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
if slices.Contains(acl.MatchUsergroups, user.Usergroup) {
|
||||
user.acls = append(user.acls, acl.Name)
|
||||
continue
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if acl.AddToEveryAction {
|
||||
return true
|
||||
}
|
||||
|
||||
if slices.Contains(actionAcls, acl.Name) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func getRelevantAcls(cfg *config.Config, actionAcls []string, user *AuthenticatedUser) []*config.AccessControlList {
|
||||
var ret []*config.AccessControlList
|
||||
|
||||
for _, acl := range cfg.AccessControlLists {
|
||||
if isACLRelevantToAction(cfg, actionAcls, acl, user) {
|
||||
ret = append(ret, acl)
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFindAction(t *testing.T) {
|
||||
c := DefaultConfig()
|
||||
|
||||
a1 := &Action{}
|
||||
a1.Title = "a1"
|
||||
c.Actions = append(c.Actions, a1)
|
||||
|
||||
a2 := &Action{
|
||||
Title: "a2",
|
||||
Arguments: []ActionArgument{
|
||||
{
|
||||
Name: "Blat",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
c.Actions = append(c.Actions, a2)
|
||||
|
||||
assert.NotNil(t, c.FindAction("a1"), "Find action a1")
|
||||
|
||||
assert.NotNil(t, c.FindAction("a2"), "Find action a2")
|
||||
assert.NotNil(t, c.FindAction("a2").FindArg("Blat"), "Find action argument")
|
||||
assert.Nil(t, c.FindAction("a2").FindArg("Blatey Cake"), "Find non-existent action argument")
|
||||
|
||||
assert.Nil(t, c.FindAction("waffles"), "Find non-existent action")
|
||||
}
|
||||
|
||||
func TestFindAcl(t *testing.T) {
|
||||
c := DefaultConfig()
|
||||
|
||||
acl1 := &AccessControlList{
|
||||
Name: "Testing ACL",
|
||||
}
|
||||
|
||||
c.AccessControlLists = append(c.AccessControlLists, acl1)
|
||||
|
||||
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")
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
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,181 +0,0 @@
|
||||
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, actionTitle string, entityPrefix string) (string, error) {
|
||||
log.WithFields(log.Fields{
|
||||
"actionTitle": actionTitle,
|
||||
"cmd": rawShellCommand,
|
||||
}).Infof("Action parse args - Before")
|
||||
|
||||
r := regexp.MustCompile("{{ *?([a-zA-Z0-9_]+?) *?}}")
|
||||
matches := r.FindAllStringSubmatch(rawShellCommand, -1)
|
||||
|
||||
for _, match := range matches {
|
||||
argValue, argProvided := values[match[1]]
|
||||
|
||||
if !argProvided {
|
||||
log.Infof("%v", values)
|
||||
return "", errors.New("Required arg not provided: " + match[1])
|
||||
}
|
||||
|
||||
err := typecheckActionArgument(match[1], argValue, action)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"name": match[1],
|
||||
"value": argValue,
|
||||
}).Debugf("Arg assigned")
|
||||
|
||||
rawShellCommand = strings.ReplaceAll(rawShellCommand, match[0], argValue)
|
||||
}
|
||||
|
||||
rawShellCommand = sv.ReplaceEntityVars(entityPrefix, rawShellCommand)
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"actionTitle": actionTitle,
|
||||
"cmd": rawShellCommand,
|
||||
}).Infof("Action parse args - After")
|
||||
|
||||
return rawShellCommand, nil
|
||||
}
|
||||
|
||||
func typecheckActionArgument(name string, value string, action *config.Action) error {
|
||||
arg := action.FindArg(name)
|
||||
|
||||
if arg == nil {
|
||||
return errors.New("Action arg not defined: " + name)
|
||||
}
|
||||
|
||||
if value == "" {
|
||||
return typecheckNull(arg)
|
||||
}
|
||||
|
||||
if len(arg.Choices) > 0 {
|
||||
return typecheckChoice(value, arg)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
switch argumentType {
|
||||
case "password":
|
||||
return nil
|
||||
case "url":
|
||||
return typeSafetyCheckUrl(name, value)
|
||||
case "datetime":
|
||||
return typeSafetyCheckDatetime(name, value)
|
||||
}
|
||||
|
||||
return typeSafetyCheckRegex(name, value, argumentType)
|
||||
}
|
||||
|
||||
func typeSafetyCheckDatetime(name string, value string) error {
|
||||
_, err := time.Parse("2006-01-02T15:04:05", value)
|
||||
|
||||
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,
|
||||
"pattern": pattern,
|
||||
}).Warn("Arg type check safety failure")
|
||||
|
||||
return errors.New("invalid argument, doesn't match " + argumentType)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func typeSafetyCheckUrl(name string, value string) error {
|
||||
_, err := url.ParseRequestURI(value)
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSanitizeUnsafe(t *testing.T) {
|
||||
assert.Nil(t, TypeSafetyCheck("", "_zomg_ c:/ haxxor ' bobby tables && rm -rf ", "very_dangerous_raw_string"))
|
||||
}
|
||||
|
||||
func TestSanitizeUnimplemented(t *testing.T) {
|
||||
err := TypeSafetyCheck("", "I am a happy little argument", "greeting_type")
|
||||
|
||||
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",
|
||||
Shell: "echo 'Tickling {{ person1name }}'",
|
||||
Arguments: []config.ActionArgument{
|
||||
{
|
||||
Name: "person1name",
|
||||
Type: "ascii",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
values := map[string]string{
|
||||
"person1name": "Fred",
|
||||
}
|
||||
|
||||
out, err := parseActionArguments(a1.Shell, values, &a1, a1.Title, "")
|
||||
|
||||
assert.Equal(t, "echo 'Tickling Fred'", out)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestArgumentNotProvided(t *testing.T) {
|
||||
a1 := config.Action{
|
||||
Title: "Do some tickles",
|
||||
Shell: "echo 'Tickling {{ personName }}'",
|
||||
Arguments: []config.ActionArgument{
|
||||
{
|
||||
Name: "person",
|
||||
Type: "ascii",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
values := map[string]string{}
|
||||
|
||||
out, err := parseActionArguments(a1.Shell, values, &a1, a1.Title, "")
|
||||
|
||||
assert.Equal(t, "", out)
|
||||
assert.Equal(t, err.Error(), "Required arg not provided: personName")
|
||||
}
|
||||
|
||||
func TestTypeSafetyCheckUrl(t *testing.T) {
|
||||
assert.Nil(t, TypeSafetyCheck("test1", "http://google.com", "url"), "Test URL: google.com")
|
||||
assert.Nil(t, TypeSafetyCheck("test2", "http://technowax.net:80?foo=bar", "url"), "Test URL: technowax.net with query arguments")
|
||||
assert.Nil(t, TypeSafetyCheck("test3", "http://localhost:80?foo=bar", "url"), "Test URL: localhost with query arguments")
|
||||
assert.NotNil(t, TypeSafetyCheck("test4", "http://lo host:80", "url"), "Test a badly formed URL")
|
||||
assert.NotNil(t, TypeSafetyCheck("test5", "12345", "url"), "Test a badly formed URL")
|
||||
assert.NotNil(t, TypeSafetyCheck("test6", "_!23;", "url"), "Test a badly formed URL")
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
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))
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
|
||||
acl "github.com/OliveTin/OliveTin/internal/acl"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
)
|
||||
|
||||
func testingExecutor() (*Executor, *config.Config) {
|
||||
cfg := config.DefaultConfig()
|
||||
|
||||
e := DefaultExecutor(cfg)
|
||||
|
||||
a1 := &config.Action{
|
||||
Title: "Do some tickles",
|
||||
Shell: "echo 'Tickling {{ person }}'",
|
||||
Arguments: []config.ActionArgument{
|
||||
{
|
||||
Name: "person",
|
||||
Type: "ascii",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cfg.Actions = append(cfg.Actions, a1)
|
||||
cfg.Sanitize()
|
||||
|
||||
return e, cfg
|
||||
}
|
||||
|
||||
func TestCreateExecutorAndExec(t *testing.T) {
|
||||
e, cfg := testingExecutor()
|
||||
|
||||
req := ExecutionRequest{
|
||||
ActionTitle: "Do some tickles",
|
||||
AuthenticatedUser: &acl.AuthenticatedUser{Username: "Mr Tickle"},
|
||||
Cfg: cfg,
|
||||
Arguments: map[string]string{
|
||||
"person": "yourself",
|
||||
},
|
||||
}
|
||||
|
||||
assert.NotNil(t, e, "Create an executor")
|
||||
|
||||
wg, _ := e.ExecRequest(&req)
|
||||
wg.Wait()
|
||||
|
||||
assert.Equal(t, int32(0), req.logEntry.ExitCode, "Exit code is zero")
|
||||
}
|
||||
|
||||
func TestExecNonExistant(t *testing.T) {
|
||||
e, cfg := testingExecutor()
|
||||
|
||||
req := ExecutionRequest{
|
||||
ActionTitle: "Waffles",
|
||||
logEntry: &InternalLogEntry{},
|
||||
Cfg: cfg,
|
||||
}
|
||||
|
||||
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 is a poop (not found)")
|
||||
}
|
||||
|
||||
func TestArgumentNameCamelCase(t *testing.T) {
|
||||
a1 := &config.Action{
|
||||
Title: "Do some tickles",
|
||||
Shell: "echo 'Tickling {{ personName }}'",
|
||||
Arguments: []config.ActionArgument{
|
||||
{
|
||||
Name: "personName",
|
||||
Type: "ascii",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
values := map[string]string{
|
||||
"personName": "Fred",
|
||||
}
|
||||
|
||||
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{
|
||||
Title: "Do some tickles",
|
||||
Shell: "echo 'Tickling {{ person_name }}'",
|
||||
Arguments: []config.ActionArgument{
|
||||
{
|
||||
Name: "person_name",
|
||||
Type: "ascii",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
values := map[string]string{
|
||||
"person_name": "Fred",
|
||||
}
|
||||
|
||||
out, err := parseActionArguments(a1.Shell, values, a1, a1.Title, "")
|
||||
|
||||
assert.Equal(t, "echo 'Tickling Fred'", out)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||