mirror of
https://github.com/OliveTin/OliveTin
synced 2025-10-30 12:57:06 +00:00
Compare commits
160 Commits
2024.06.01
...
2025.3.28
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
1d9502d800 | ||
|
|
c0a18f82a5 | ||
|
|
9723a38ca1 | ||
|
|
8258a758d2 | ||
|
|
044613ae9a | ||
|
|
76c9171356 | ||
|
|
f8c330aae3 | ||
|
|
d4bd7dd586 | ||
|
|
fa20f6a63a | ||
|
|
b9ce695616 | ||
|
|
3f1dbf1130 | ||
|
|
6413e51cf5 | ||
|
|
defcf6d26e | ||
|
|
2671983c43 | ||
|
|
dc9653307b | ||
|
|
eb91eb33d5 | ||
|
|
ddb803d9b5 | ||
|
|
c9095b4d67 | ||
|
|
40158eda71 | ||
|
|
bbbbfceeb3 | ||
|
|
9ca1940834 | ||
|
|
37160a91f3 | ||
|
|
274d036f74 | ||
|
|
1fe0e49adb | ||
|
|
7a7a07d9ad | ||
|
|
5b5fca0837 | ||
|
|
fab0264d9b | ||
|
|
8d839ee6ce | ||
|
|
90efbf3159 | ||
|
|
17dd1b4158 | ||
|
|
e5f6d8ff50 | ||
|
|
e183910b88 | ||
|
|
8cd5b9fb46 | ||
|
|
6958445f83 | ||
|
|
ef91294d5e | ||
|
|
a36f286f8a | ||
|
|
d9e921950f | ||
|
|
ffcd19e748 | ||
|
|
d0eb132b95 | ||
|
|
1cf971c092 | ||
|
|
49f745be68 | ||
|
|
b50824a705 | ||
|
|
69d1cc75a7 | ||
|
|
a54ea505c9 | ||
|
|
a1563b72ae | ||
|
|
31d7168aac | ||
|
|
09016e1d5f | ||
|
|
652882350c | ||
|
|
20c4423799 | ||
|
|
bb90a5da92 | ||
|
|
3ca3a2dd3c | ||
|
|
510c48e1af | ||
|
|
3ac809c234 | ||
|
|
9dd33bc3f9 | ||
|
|
897cc0e034 | ||
|
|
482ef0e5e8 | ||
|
|
e0678fc0a9 | ||
|
|
9cb5574b99 | ||
|
|
c18b91f684 | ||
|
|
6622a6ded4 | ||
|
|
fb6aaa52c7 | ||
|
|
a1adc2a85d | ||
|
|
3d9cb621dd | ||
|
|
943b4c75aa | ||
|
|
fb972fae55 | ||
|
|
eb4f28dfda | ||
|
|
e36fedf4b2 | ||
|
|
362a97c59e | ||
|
|
c82beb61a9 | ||
|
|
00a8a0bf69 | ||
|
|
ffc17dd73b |
@@ -1,31 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
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
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
|
||||
labels:
|
||||
- "type: bug"
|
||||
- "waiting-on-developer"
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
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
|
||||
labels:
|
||||
- "type: feature-request"
|
||||
- "waiting-on-developer"
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/support_request.md
vendored
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
|
||||
labels:
|
||||
- "type: support"
|
||||
- "waiting-on-developer"
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
11
.github/PULL_REQUEST_TEMPLATE.md
vendored
11
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -14,9 +14,10 @@ Helpful information to understand the project can be found here: [CONTRIBUTING](
|
||||
Please put a X in the boxes as evidence of reading through the checklist.
|
||||
|
||||
- [ ] I have forked the project, and raised this PR on a feature branch.
|
||||
- [ ] `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.
|
||||
|
||||
38
.github/workflows/build-buf.yml
vendored
Normal file
38
.github/workflows/build-buf.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Buf CI
|
||||
on:
|
||||
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
|
||||
|
||||
- name: grpc
|
||||
run: make -w grpc
|
||||
|
||||
- name: make service
|
||||
run: make -w service
|
||||
|
||||
- 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 }}
|
||||
16
.github/workflows/build-snapshot.yml
vendored
16
.github/workflows/build-snapshot.yml
vendored
@@ -4,8 +4,6 @@ name: "Build Snapshot"
|
||||
on:
|
||||
push:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build-snapshot:
|
||||
@@ -33,26 +31,29 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '^1.18.0'
|
||||
go-version-file: 'service/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 service
|
||||
run: make -w service
|
||||
|
||||
- name: make webui
|
||||
run: make -w webui-dist
|
||||
|
||||
- name: unit tests
|
||||
run: make -w daemon-unittests
|
||||
run: make -w service-unittests
|
||||
|
||||
- name: integration tests
|
||||
run: cd integration-tests && make -w
|
||||
|
||||
- name: goreleaser
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
@@ -70,6 +71,7 @@ jobs:
|
||||
|
||||
- name: Archive integration tests
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
if: always()
|
||||
with:
|
||||
name: "OliveTin-integration-tests-${{ env.DATE }}-${{ github.sha }}"
|
||||
path: |
|
||||
|
||||
13
.github/workflows/build-tag.yml
vendored
13
.github/workflows/build-tag.yml
vendored
@@ -31,17 +31,20 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '^1.18.0'
|
||||
go-version-file: 'service/go.mod'
|
||||
cache: true
|
||||
|
||||
- name: Print go version
|
||||
run: go version
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_KEY }}
|
||||
|
||||
- name: Login to ghcr
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -54,11 +57,11 @@ jobs:
|
||||
run: make -w webui-dist
|
||||
|
||||
- name: goreleaser
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --clean --parallelism 1
|
||||
args: release --clean --timeout 60m
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.CONTAINER_TOKEN }}
|
||||
|
||||
|
||||
9
.github/workflows/codeql-analysis.yml
vendored
9
.github/workflows/codeql-analysis.yml
vendored
@@ -44,6 +44,15 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: 'service/go.mod'
|
||||
cache: true
|
||||
|
||||
- name: grpc
|
||||
run: make -w grpc
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
|
||||
11
.github/workflows/codestyle.yml
vendored
11
.github/workflows/codestyle.yml
vendored
@@ -21,14 +21,17 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '^1.18.0'
|
||||
go-version-file: 'service/go.mod'
|
||||
cache: true
|
||||
|
||||
- name: Print go version
|
||||
run: go version
|
||||
|
||||
- name: deps
|
||||
run: make -w grpc
|
||||
|
||||
- name: daemon
|
||||
run: make -w daemon-codestyle
|
||||
- name: service
|
||||
run: make -wC service codestyle
|
||||
|
||||
- name: webui
|
||||
run: make -w webui-codestyle
|
||||
run: make -wC webui.dev codestyle
|
||||
|
||||
4
.github/workflows/devskim.yml
vendored
4
.github/workflows/devskim.yml
vendored
@@ -23,12 +23,12 @@ jobs:
|
||||
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
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}`);
|
||||
}
|
||||
}
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,10 +1,10 @@
|
||||
**/*.swp
|
||||
**/*.swo
|
||||
gen/
|
||||
/OliveTin
|
||||
/OliveTin.armhf
|
||||
/OliveTin.exe
|
||||
reports
|
||||
service/gen/
|
||||
service/OliveTin
|
||||
service/OliveTin.armhf
|
||||
service/OliveTin.exe
|
||||
service/reports
|
||||
releases/
|
||||
dist/
|
||||
installation-id.txt
|
||||
@@ -12,3 +12,4 @@ tmp/
|
||||
webui/
|
||||
webui.dev/node_modules
|
||||
webui.dev/.parcel-cache
|
||||
custom-webui
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
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
|
||||
@@ -39,7 +43,7 @@ builds:
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ .Branch }}-{{ .ShortCommit }}"
|
||||
version_template: "{{ .Branch }}-{{ .ShortCommit }}"
|
||||
changelog:
|
||||
sort: asc
|
||||
groups:
|
||||
@@ -62,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:
|
||||
@@ -98,6 +96,7 @@ dockers:
|
||||
- webui
|
||||
- var/entities/
|
||||
- config.yaml
|
||||
- var/helper-actions/
|
||||
|
||||
- image_templates:
|
||||
- "docker.io/jamesread/olivetin:{{ .Tag }}-arm64"
|
||||
@@ -114,6 +113,7 @@ dockers:
|
||||
- webui
|
||||
- var/entities/
|
||||
- config.yaml
|
||||
- var/helper-actions/
|
||||
|
||||
docker_manifests:
|
||||
- name_template: docker.io/jamesread/olivetin:{{ .Version }}
|
||||
@@ -151,7 +151,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/*
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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].
|
||||
|
||||
== 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. 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.
|
||||
|
||||
== 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,13 +30,21 @@ 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 - Fedora
|
||||
=== Dev environment setup and clean build
|
||||
|
||||
```
|
||||
# Step1: setup compile env
|
||||
# - Fedora
|
||||
dnf install git go protobuf-compiler make -y
|
||||
# - Windows with chocolatey
|
||||
choco install git go protoc make python nodejs-lts -y
|
||||
|
||||
# Step2: clone and setup repo
|
||||
git clone https://github.com/OliveTin/OliveTin.git
|
||||
cd OliveTin
|
||||
make githooks
|
||||
|
||||
# Step3: compile binary for current dev env (OS, ARCH)
|
||||
# `make grpc` will also run `make go-tools`, which installs "buf". This binary
|
||||
# will be put in your GOPATH/bin/, which should be on your path. buf is used to
|
||||
# generate the protobuf / grpc stubs.
|
||||
|
||||
21
Dockerfile
21
Dockerfile
@@ -1,7 +1,13 @@
|
||||
FROM --platform=linux/amd64 registry.fedoraproject.org/fedora-minimal:38-x86_64
|
||||
FROM --platform=linux/amd64 registry.fedoraproject.org/fedora-minimal:40-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:40-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 \
|
||||
&& \
|
||||
@@ -11,9 +17,17 @@ RUN mkdir -p /config /config/entities/ /var/www/olivetin \
|
||||
shadow-utils \
|
||||
apprise \
|
||||
jq \
|
||||
docker \
|
||||
git \
|
||||
&& 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
|
||||
@@ -24,6 +38,7 @@ VOLUME /config
|
||||
|
||||
COPY OliveTin /usr/bin/OliveTin
|
||||
COPY webui /var/www/olivetin/
|
||||
COPY var/helper-actions/* /usr/bin/
|
||||
|
||||
USER olivetin
|
||||
|
||||
|
||||
@@ -1,19 +1,33 @@
|
||||
FROM --platform=linux/arm64 registry.fedoraproject.org/fedora-minimal:38-aarch64
|
||||
FROM --platform=linux/arm64 registry.fedoraproject.org/fedora-minimal:40-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:40-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 \
|
||||
shadow-utils \
|
||||
apprise \
|
||||
jq \
|
||||
docker \
|
||||
git \
|
||||
&& 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
|
||||
@@ -24,6 +38,7 @@ VOLUME /config
|
||||
|
||||
COPY OliveTin /usr/bin/OliveTin
|
||||
COPY webui /var/www/olivetin/
|
||||
COPY var/helper-actions/* /usr/bin/
|
||||
|
||||
USER olivetin
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ VOLUME /config
|
||||
|
||||
COPY OliveTin /usr/bin/OliveTin
|
||||
COPY webui /var/www/olivetin/
|
||||
COPY var/helper-actions/* /usr/bin/
|
||||
|
||||
USER olivetin
|
||||
|
||||
|
||||
50
Jenkinsfile
vendored
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
68
Makefile
68
Makefile
@@ -1,45 +1,26 @@
|
||||
compile: daemon-compile-x64-lin
|
||||
define delete-files
|
||||
python -c "import shutil;shutil.rmtree('$(1)', ignore_errors=True)"
|
||||
endef
|
||||
|
||||
daemon-compile-armhf:
|
||||
GOARCH=arm GOARM=6 go build -o OliveTin.armhf github.com/OliveTin/OliveTin/cmd/OliveTin
|
||||
service:
|
||||
$(MAKE) -wC service
|
||||
|
||||
daemon-compile-x64-lin:
|
||||
GOOS=linux go build -o OliveTin github.com/OliveTin/OliveTin/cmd/OliveTin
|
||||
|
||||
daemon-compile-x64-win:
|
||||
GOOS=windows GOARCH=amd64 go build -o OliveTin.exe github.com/OliveTin/OliveTin/cmd/OliveTin
|
||||
|
||||
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:
|
||||
mkdir -p 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:
|
||||
cp -v .githooks/* .git/hooks/
|
||||
$(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
|
||||
|
||||
proto: grpc
|
||||
|
||||
grpc: go-tools
|
||||
buf generate
|
||||
$(MAKE) -wC proto
|
||||
|
||||
dist: protoc
|
||||
|
||||
@@ -67,17 +48,22 @@ devrun: compile
|
||||
devcontainer: compile podman-image podman-container
|
||||
|
||||
webui-codestyle:
|
||||
cd webui.dev && npm install
|
||||
cd webui.dev && ./node_modules/.bin/eslint main.js js/*
|
||||
cd webui.dev && ./node_modules/.bin/stylelint style.css
|
||||
make -wC webui.dev codestyle
|
||||
|
||||
webui-dist:
|
||||
rm -rf webui webui.dev/dist
|
||||
$(call delete-files,webui)
|
||||
$(call delete-files,webui.dev/dist)
|
||||
cd webui.dev && npm install
|
||||
cd webui.dev && parcel build --public-url "." && mv dist ../webui
|
||||
cp webui.dev/*.png webui/
|
||||
cd webui.dev && npx parcel build --public-url "."
|
||||
python -c "import shutil;shutil.move('webui.dev/dist', 'webui')"
|
||||
python -c "import shutil;import glob;[shutil.copy(f, 'webui') for f in glob.glob('webui.dev/*.png')]"
|
||||
|
||||
clean:
|
||||
rm -rf dist OliveTin OliveTin.armhf OliveTin.exe reports gen
|
||||
$(call delete-files,dist)
|
||||
$(call delete-files,OliveTin)
|
||||
$(call delete-files,OliveTin.armhf)
|
||||
$(call delete-files,OliveTin.exe)
|
||||
$(call delete-files,reports)
|
||||
$(call delete-files,gen)
|
||||
|
||||
.PHONY: grpc
|
||||
.PHONY: grpc proto service
|
||||
|
||||
57
README.md
57
README.md
@@ -4,6 +4,7 @@
|
||||
|
||||
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)
|
||||
@@ -14,6 +15,8 @@ OliveTin gives **safe** and **simple** access to predefined shell commands from
|
||||
<img alt = "screenshot" src = "https://github.com/OliveTin/OliveTin/blob/main/var/marketing/screenshotDesktop.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;
|
||||
@@ -67,56 +70,6 @@ Mobile screen size (responsive layout);
|
||||
|
||||
## 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!
|
||||
|
||||
7
buf.lock
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
|
||||
98
config.yaml
98
config.yaml
@@ -5,20 +5,22 @@
|
||||
# Listen on all addresses available, port 1337
|
||||
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||
|
||||
|
||||
# Choose from INFO (default), WARN and DEBUG
|
||||
logLevel: "INFO"
|
||||
|
||||
# Checking for updates https://docs.olivetin.app/reference/updateChecks.html
|
||||
checkForUpdates: false
|
||||
|
||||
# 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
|
||||
@@ -40,11 +42,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
|
||||
@@ -52,7 +59,7 @@ actions:
|
||||
# runs for too long.
|
||||
- title: Run backup script
|
||||
shell: /opt/backupScript.sh
|
||||
shellAfterCompleted: "apprise -t 'Notification: Backup script completed' -b 'The backup script completed with code {{ exitCode}}. The log is: \n {{ stdout }} '"
|
||||
shellAfterCompleted: "apprise -t 'Notification: Backup script completed' -b 'The backup script completed with code {{ exitCode}}. The log is: \n {{ output }} '"
|
||||
maxConcurrent: 1
|
||||
timeout: 10
|
||||
icon: backup
|
||||
@@ -61,8 +68,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
|
||||
@@ -85,7 +93,7 @@ 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 }}
|
||||
@@ -100,25 +108,56 @@ 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/reference/reference_themes_for_users.html
|
||||
- title: Get OliveTin Theme
|
||||
shell: olivetin-get-theme {{ themeGitRepo }} {{ themeFolderName }}
|
||||
icon: theme
|
||||
arguments:
|
||||
- name: themeGitRepo
|
||||
title: Theme's Git Repository
|
||||
description: Find new themes at https://olivetin.app/themes
|
||||
type: url
|
||||
|
||||
- name: themeFolderName
|
||||
title: Theme's Folder Name
|
||||
type: ascii_identifier
|
||||
|
||||
# Sometimes you want to run actions on other servers - don't overcomplicate
|
||||
# it, just use SSH!
|
||||
# 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.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/ssh-manual.html
|
||||
- title: "Setup easy SSH"
|
||||
icon: ssh
|
||||
shell: olivetin-setup-easy-ssh
|
||||
popupOnStart: execution-dialog
|
||||
|
||||
# Here's how to use SSH with the "easy" config, to restart a service on
|
||||
# another server.
|
||||
#
|
||||
# Docs: https://docs.olivetin.app/action_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 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
|
||||
@@ -131,19 +170,19 @@ 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
|
||||
timeout: 120
|
||||
|
||||
# The following actions are "dummy" actions, used in a Dashboard. As long as
|
||||
# you have these referenced in a dashboard, they will not who up in the
|
||||
# you have these referenced in a dashboard, they will not show up in the
|
||||
# `actions` view.
|
||||
- title: Ping hypervisor1
|
||||
shell: echo "hypervisor1 online"
|
||||
@@ -167,13 +206,13 @@ actions:
|
||||
icon: box
|
||||
shell: docker start {{ container.Names }}
|
||||
entity: container
|
||||
trigger: Update container entity file
|
||||
triggers: ["Update container entity file"]
|
||||
|
||||
- title: Stop {{ container.Names }}
|
||||
icon: box
|
||||
shell: docker stop {{ container.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
|
||||
@@ -203,13 +242,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
|
||||
|
||||
@@ -221,20 +260,25 @@ 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
|
||||
contents:
|
||||
# The contents of a dashboard will try to look for an action with a
|
||||
# matching title IF the `contents: ` property is empty.
|
||||
- title: Ping All Servers
|
||||
|
||||
# If you create an item with some "contents:", OliveTin will show that as
|
||||
# directory.
|
||||
- title: Hypervisors
|
||||
- title: All Servers
|
||||
type: fieldset
|
||||
contents:
|
||||
- title: Ping hypervisor1
|
||||
- title: Ping hypervisor2
|
||||
# The contents of a dashboard will try to look for an action with a
|
||||
# matching title IF the `contents: ` property is empty.
|
||||
- title: Ping All Servers
|
||||
|
||||
# If you create an item with some "contents:", OliveTin will show that as
|
||||
# directory.
|
||||
- title: Hypervisors
|
||||
contents:
|
||||
- title: Ping hypervisor1
|
||||
- title: Ping hypervisor2
|
||||
|
||||
# If you specify `type: fieldset` and some `contents`, it will show your
|
||||
# actions grouped together without a folder.
|
||||
|
||||
133
go.mod
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.0.2+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.10.1 // 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
|
||||
)
|
||||
@@ -4,7 +4,11 @@ test-install:
|
||||
npm install --no-fund
|
||||
|
||||
test-run:
|
||||
./node_modules/.bin/mocha -t 10000
|
||||
npx mocha -t 10000
|
||||
|
||||
find-flakey-tests:
|
||||
echo "Running test-run infinately"
|
||||
sh -c "while make test-run; do :; done"
|
||||
|
||||
nginx:
|
||||
podman-compose up -d nginx
|
||||
|
||||
@@ -24,12 +24,12 @@ actions:
|
||||
shell: sleep 5
|
||||
icon: "😪"
|
||||
|
||||
- title: date-popup
|
||||
shell: date
|
||||
- title: dir-popup
|
||||
shell: dir
|
||||
popupOnStart: execution-dialog-stdout-only
|
||||
|
||||
- title: date-passive
|
||||
shell: date
|
||||
- title: cd-passive
|
||||
shell: cd
|
||||
|
||||
- title: "Run Ansible Playbook"
|
||||
icon: "🇦"
|
||||
|
||||
16
integration-tests/configs/prometheus/config.yaml
Normal file
16
integration-tests/configs/prometheus/config.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
#
|
||||
# Integration Test Config: General
|
||||
#
|
||||
|
||||
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||
|
||||
logLevel: "DEBUG"
|
||||
checkForUpdates: false
|
||||
|
||||
prometheus:
|
||||
enabled: true
|
||||
defaultGoMetrics: false
|
||||
|
||||
actions:
|
||||
- title: Hello OliveTin
|
||||
shell: echo "Hello OliveTin"
|
||||
14
integration-tests/configs/sleep/config.yaml
Normal file
14
integration-tests/configs/sleep/config.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
# Integration Test Config: Sleep
|
||||
#
|
||||
|
||||
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||
|
||||
logLevel: "DEBUG"
|
||||
checkForUpdates: false
|
||||
|
||||
actions:
|
||||
- title: Sleep
|
||||
shell: sleep 10
|
||||
popupOnStart: execution-dialog
|
||||
timeout: 9
|
||||
@@ -1,27 +1,71 @@
|
||||
import { By } from 'selenium-webdriver'
|
||||
import fs from 'fs'
|
||||
import { expect } from 'chai'
|
||||
import { Condition } from 'selenium-webdriver'
|
||||
|
||||
export async function getActionButtons (webdriver) {
|
||||
return await webdriver.findElement(By.id('contentActions')).findElements(By.tagName('button'))
|
||||
}
|
||||
|
||||
export function takeScreenshot (webdriver) {
|
||||
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(/[\(\)\|\*\<\>\:]/g, "_")
|
||||
title = 'failed-test.' + title
|
||||
|
||||
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() {
|
||||
const body = await webdriver.findElement(By.tagName('body'))
|
||||
const attr = await body.getAttribute('initial-marshal-complete')
|
||||
await webdriver.get(runner.baseUrl())
|
||||
await webdriver.wait(new Condition('wait for initial-marshal-complete', async function() {
|
||||
const body = await webdriver.findElement(By.tagName('body'))
|
||||
const attr = await body.getAttribute('initial-marshal-complete')
|
||||
|
||||
if (attr == 'true') {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}))
|
||||
if (attr == 'true') {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
export async function requireExecutionDialogStatus (webdriver, expected) {
|
||||
// It seems that webdriver will not give us text if domStatus is hidden (which it will be until complete)
|
||||
await webdriver.executeScript('window.executionDialog.domExecutionDetails.hidden = false')
|
||||
|
||||
await webdriver.wait(new Condition('wait for action to be running', async function () {
|
||||
const actual = await webdriver.executeScript('return window.executionDialog.domStatus.getText()')
|
||||
|
||||
if (actual === expected) {
|
||||
return true
|
||||
} else {
|
||||
console.log('Waiting for domStatus text to be: ', expected, ', it is currently: ', actual)
|
||||
console.log(await webdriver.executeScript('return window.executionDialog.res'))
|
||||
return false
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
export async function findExecutionDialog (webdriver) {
|
||||
return webdriver.findElement(By.id('execution-results-popup'))
|
||||
}
|
||||
|
||||
export async function getActionButton (webdriver, title) {
|
||||
const buttons = await webdriver.findElements(By.css('[title="' + title + '"]'))
|
||||
|
||||
expect(buttons).to.have.length(1)
|
||||
|
||||
return buttons[0]
|
||||
}
|
||||
|
||||
1303
integration-tests/package-lock.json
generated
1303
integration-tests/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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": "^5.2.0",
|
||||
"eslint": "^9.22.0",
|
||||
"mocha": "^11.1.0",
|
||||
"selenium-webdriver": "^4.29.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"wait-on": "^7.2.0"
|
||||
"wait-on": "^8.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@ class OliveTinTestRunner {
|
||||
baseUrl() {
|
||||
return this.BASE_URL
|
||||
}
|
||||
|
||||
metricsUrl() {
|
||||
return new URL('metrics', this.baseUrl());
|
||||
}
|
||||
}
|
||||
|
||||
class OliveTinTestRunnerStartLocalProcess extends OliveTinTestRunner {
|
||||
@@ -32,9 +36,17 @@ class OliveTinTestRunnerStartLocalProcess extends OliveTinTestRunner {
|
||||
let stdout = ""
|
||||
let stderr = ""
|
||||
|
||||
this.ot = spawn('./../OliveTin', ['-configdir', 'configs/' + cfg + '/'])
|
||||
console.log(" OliveTin starting local process...")
|
||||
|
||||
const logStdout = process.env.OLIVETIN_TEST_RUNNER_LOG_STDOUT === '1'
|
||||
this.ot = spawn('./../service/OliveTin', ['-configdir', 'configs/' + cfg + '/'])
|
||||
|
||||
let logStdout = false
|
||||
|
||||
if (process.env.CI === 'true') {
|
||||
logStdout = true;
|
||||
} else {
|
||||
logStdout = process.env.OLIVETIN_TEST_RUNNER_LOG_STDOUT === '1'
|
||||
}
|
||||
|
||||
this.ot.stdout.on('data', (data) => {
|
||||
stdout += data
|
||||
@@ -64,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]
|
||||
})
|
||||
@@ -85,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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,6 +16,10 @@ describe('config: entities', function () {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
takeScreenshotOnFailure(this.currentTest, webdriver);
|
||||
});
|
||||
|
||||
it('Entity buttons are rendered', async function() {
|
||||
await getRootAndWait()
|
||||
|
||||
|
||||
@@ -2,7 +2,11 @@ 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,
|
||||
} from '../lib/elements.js'
|
||||
|
||||
describe('config: general', function () {
|
||||
before(async function () {
|
||||
@@ -13,6 +17,10 @@ describe('config: general', function () {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
takeScreenshotOnFailure(this.currentTest, webdriver);
|
||||
});
|
||||
|
||||
it('Page title', async function () {
|
||||
await webdriver.get(runner.baseUrl())
|
||||
|
||||
@@ -44,42 +52,42 @@ describe('config: general', function () {
|
||||
expect(buttons).to.have.length(8)
|
||||
})
|
||||
|
||||
it('Start date action (popup)', async function() {
|
||||
it('Start dir action (popup)', async function () {
|
||||
await getRootAndWait()
|
||||
|
||||
const buttons = await webdriver.findElements(By.css('[title="date-popup"]'))
|
||||
const buttons = await webdriver.findElements(By.css('[title="dir-popup"]'))
|
||||
|
||||
expect(buttons).to.have.length(1)
|
||||
|
||||
const buttonDate = buttons[0]
|
||||
const buttonCMD = buttons[0]
|
||||
|
||||
expect(buttonDate).to.not.be.null
|
||||
expect(buttonCMD).to.not.be.null
|
||||
|
||||
buttonDate.click()
|
||||
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 title.getAttribute('innerText')).to.be.equal('date-popup')
|
||||
expect(await webdriver.wait(until.elementTextIs(title, 'dir-popup'), 2000))
|
||||
|
||||
const dialogErr = await webdriver.findElement(By.id('big-error'))
|
||||
expect(dialogErr).to.not.be.null
|
||||
expect(await dialogErr.isDisplayed()).to.be.false
|
||||
})
|
||||
|
||||
it('Start date action (passive)', async function() {
|
||||
it('Start cd action (passive)', async function () {
|
||||
await getRootAndWait()
|
||||
|
||||
const buttons = await webdriver.findElements(By.css('[title="date-passive"]'))
|
||||
const buttons = await webdriver.findElements(By.css('[title="cd-passive"]'))
|
||||
|
||||
expect(buttons).to.have.length(1)
|
||||
|
||||
const buttonDate = buttons[0]
|
||||
const buttonCMD = buttons[0]
|
||||
|
||||
expect(buttonDate).to.not.be.null
|
||||
expect(buttonCMD).to.not.be.null
|
||||
|
||||
buttonDate.click()
|
||||
buttonCMD.click()
|
||||
|
||||
const dialog = await webdriver.findElement(By.id('execution-results-popup'))
|
||||
expect(await dialog.isDisplayed()).to.be.false
|
||||
@@ -88,6 +96,7 @@ describe('config: general', function () {
|
||||
expect(await title.getAttribute('innerText')).to.be.equal('?')
|
||||
|
||||
const dialogErr = await webdriver.findElement(By.id('big-error'))
|
||||
console.log("big error is: " + dialogErr.innerHTML)
|
||||
expect(dialogErr).to.not.be.null
|
||||
expect(await dialogErr.isDisplayed()).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,6 +17,10 @@ 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())
|
||||
|
||||
|
||||
@@ -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,6 +16,10 @@ describe('config: hiddenNav', function () {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
takeScreenshotOnFailure(this.currentTest, webdriver);
|
||||
});
|
||||
|
||||
it('nav is hidden', async () => {
|
||||
await webdriver.get(runner.baseUrl())
|
||||
|
||||
|
||||
@@ -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 {
|
||||
getRootAndWait,
|
||||
getActionButtons,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../lib/elements.js'
|
||||
|
||||
|
||||
describe('config: multipleDropdowns', function () {
|
||||
before(async function () {
|
||||
@@ -12,6 +17,10 @@ describe('config: multipleDropdowns', function () {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
takeScreenshotOnFailure(this.currentTest, webdriver);
|
||||
});
|
||||
|
||||
it('Multiple dropdowns are possible', async function() {
|
||||
await getRootAndWait()
|
||||
|
||||
|
||||
40
integration-tests/test/prometheus.mjs
Normal file
40
integration-tests/test/prometheus.mjs
Normal file
@@ -0,0 +1,40 @@
|
||||
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 () {
|
||||
before(async function () {
|
||||
await runner.start('prometheus')
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
takeScreenshotOnFailure(this.currentTest, webdriver);
|
||||
});
|
||||
|
||||
it('Metrics are available with correct types', async () => {
|
||||
webdriver.get(runner.metricsUrl())
|
||||
const prometheusOutput = await webdriver.findElement(By.tagName('pre')).getText()
|
||||
|
||||
expect(prometheusOutput).to.not.be.null
|
||||
metrics.forEach(({name, type, desc}) => {
|
||||
const metaLines = `# HELP ${name} ${desc}\n`
|
||||
+ `# TYPE ${name} ${type}\n`
|
||||
expect(prometheusOutput).to.match(new RegExp(metaLines))
|
||||
})
|
||||
})
|
||||
})
|
||||
49
integration-tests/test/sleep.js
Normal file
49
integration-tests/test/sleep.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as process from 'node:process'
|
||||
import { describe, it, before, after } from 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import { By, Condition } from 'selenium-webdriver'
|
||||
import {
|
||||
takeScreenshot,
|
||||
takeScreenshotOnFailure,
|
||||
findExecutionDialog,
|
||||
requireExecutionDialogStatus,
|
||||
getRootAndWait,
|
||||
getActionButton
|
||||
} from '../lib/elements.js'
|
||||
|
||||
describe('config: sleep', function () {
|
||||
before(async function () {
|
||||
await runner.start('sleep')
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
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()
|
||||
|
||||
expect(await dialog.isDisplayed()).to.be.true
|
||||
|
||||
await requireExecutionDialogStatus(webdriver, "unknown")
|
||||
|
||||
const killButton = await webdriver.findElement(By.id('execution-dialog-kill-action'))
|
||||
expect(killButton).to.not.be.undefined
|
||||
|
||||
await killButton.click()
|
||||
|
||||
await requireExecutionDialogStatus(webdriver, "Completed")
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,8 @@
|
||||
import { expect } from 'chai'
|
||||
import {
|
||||
getRootAndWait,
|
||||
takeScreenshotOnFailure,
|
||||
} from '../lib/elements.js'
|
||||
|
||||
describe('config: trustedHeader', function () {
|
||||
before(async function () {
|
||||
@@ -9,7 +13,13 @@ describe('config: trustedHeader', function () {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
takeScreenshotOnFailure(this.currentTest, webdriver);
|
||||
});
|
||||
|
||||
it('req with X-User', async () => {
|
||||
await getRootAndWait()
|
||||
|
||||
const req = await fetch(runner.baseUrl() + '/api/WhoAmI', {
|
||||
headers: {
|
||||
"X-User": "fred",
|
||||
|
||||
@@ -1,165 +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"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// IsAllowedLogs checks if a AuthenticatedUser is allowed to view an action's logs
|
||||
func IsAllowedLogs(cfg *config.Config, user *AuthenticatedUser, action *config.Action) bool {
|
||||
for _, acl := range getRelevantAcls(cfg, action.Acls, user) {
|
||||
if acl.Permissions.Logs {
|
||||
logAclMatched(cfg, "isAllowedLogs", user, action, acl)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
logAclNotMatched(cfg, "isAllowedLogs", user, action)
|
||||
|
||||
return cfg.DefaultPermissions.Logs
|
||||
}
|
||||
|
||||
// IsAllowedExec checks if a AuthenticatedUser is allowed to execute an Action
|
||||
func IsAllowedExec(cfg *config.Config, user *AuthenticatedUser, action *config.Action) bool {
|
||||
for _, acl := range getRelevantAcls(cfg, action.Acls, user) {
|
||||
if acl.Permissions.Exec {
|
||||
logAclMatched(cfg, "isAllowedExec", user, action, acl)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
logAclNotMatched(cfg, "isAllowedExec", user, action)
|
||||
|
||||
return cfg.DefaultPermissions.Exec
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
for _, acl := range getRelevantAcls(cfg, action.Acls, user) {
|
||||
if acl.Permissions.View {
|
||||
logAclMatched(cfg, "isAllowedView", user, action, acl)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
logAclNotMatched(cfg, "isAllowedView", user, action)
|
||||
|
||||
return cfg.DefaultPermissions.View
|
||||
}
|
||||
|
||||
func getMetdataKeyOrEmpty(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 {
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
|
||||
ret := &AuthenticatedUser{
|
||||
Username: "guest",
|
||||
Usergroup: "guest",
|
||||
}
|
||||
|
||||
if ok {
|
||||
ret.Username = getMetdataKeyOrEmpty(md, "username")
|
||||
ret.Usergroup = getMetdataKeyOrEmpty(md, "usergroup")
|
||||
}
|
||||
|
||||
buildUserAcls(cfg, ret)
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"username": ret.Username,
|
||||
"usergroup": ret.Usergroup,
|
||||
}).Debugf("UserFromContext")
|
||||
|
||||
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,138 +0,0 @@
|
||||
package updatecheck
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
installationinfo "github.com/OliveTin/OliveTin/internal/installationinfo"
|
||||
"github.com/google/uuid"
|
||||
"github.com/robfig/cron/v3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
type updateRequest struct {
|
||||
CurrentVersion string
|
||||
CurrentCommit string
|
||||
OS string
|
||||
Arch string
|
||||
InstallationID string
|
||||
InContainer bool
|
||||
}
|
||||
|
||||
// AvailableVersion is updated when checking with the update service.
|
||||
var AvailableVersion = "none"
|
||||
|
||||
// CurrentVersion is set by the main cmd (which is in tern set as a compile constant)
|
||||
var CurrentVersion = "?"
|
||||
|
||||
func installationID(filename string) string {
|
||||
var content string
|
||||
contentBytes, err := os.ReadFile(filename)
|
||||
|
||||
if err != nil {
|
||||
fileHandle, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0644)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Could not read + create installation ID file: %v", err)
|
||||
return "cant-create"
|
||||
}
|
||||
|
||||
content = uuid.NewString()
|
||||
fileHandle.WriteString(content)
|
||||
fileHandle.Close()
|
||||
} else {
|
||||
content = string(contentBytes)
|
||||
|
||||
_, err := uuid.Parse(content)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Invalid installation ID, %v", err)
|
||||
content = "invalid-installation-id"
|
||||
}
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"content": content,
|
||||
"from": filename,
|
||||
}).Infof("Installation ID")
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
// StartUpdateChecker will start a job that runs periodically, checking
|
||||
// for updates.
|
||||
func StartUpdateChecker(currentVersion string, currentCommit string, cfg *config.Config, configDir string) {
|
||||
CurrentVersion = currentVersion
|
||||
|
||||
if !cfg.CheckForUpdates {
|
||||
log.Warn("Update checking is disabled")
|
||||
return
|
||||
}
|
||||
|
||||
payload := updateRequest{
|
||||
CurrentVersion: currentVersion,
|
||||
CurrentCommit: currentCommit,
|
||||
OS: installationinfo.Runtime.OS,
|
||||
Arch: installationinfo.Runtime.Arch,
|
||||
InstallationID: installationID(configDir + "/installation-id.txt"),
|
||||
InContainer: installationinfo.Runtime.InContainer,
|
||||
}
|
||||
|
||||
s := cron.New(cron.WithSeconds())
|
||||
|
||||
// Several values have been tried here.
|
||||
// 1st: Every 24h - very spammy.
|
||||
// 2nd: Every 7d - (168 hours - much more reasonable, but it checks in at the same time/day each week.
|
||||
// Current: Every 100h is not so spammy, and has the advantage that the checkin time "shifts" hours.
|
||||
s.AddFunc("@every 100h", func() {
|
||||
actualCheckForUpdate(payload)
|
||||
})
|
||||
|
||||
go actualCheckForUpdate(payload) // On startup
|
||||
|
||||
go s.Start()
|
||||
}
|
||||
|
||||
func doRequest(jsonUpdateRequest []byte) string {
|
||||
req, err := http.NewRequest("POST", "http://update-check.olivetin.app", bytes.NewBuffer(jsonUpdateRequest))
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Update check failed %v", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Update check failed %v", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
newVersion, _ := io.ReadAll(resp.Body)
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
return string(newVersion)
|
||||
}
|
||||
|
||||
func actualCheckForUpdate(payload updateRequest) {
|
||||
jsonUpdateRequest, err := json.Marshal(payload)
|
||||
|
||||
log.Debugf("Update request payload: %+v", payload)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Update check failed %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
AvailableVersion = doRequest(jsonUpdateRequest)
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"NewVersion": AvailableVersion,
|
||||
}).Infof("Update check complete")
|
||||
}
|
||||
4
proto/Makefile
Normal file
4
proto/Makefile
Normal file
@@ -0,0 +1,4 @@
|
||||
buf:
|
||||
buf generate
|
||||
|
||||
.PHONY: buf
|
||||
@@ -1,15 +1,15 @@
|
||||
version: v1
|
||||
plugins:
|
||||
- name: go
|
||||
out: gen/grpc/
|
||||
out: ../service/gen/grpc/
|
||||
opt: paths=source_relative
|
||||
|
||||
- name: go-grpc
|
||||
out: gen/grpc/
|
||||
out: ../service/gen/grpc/
|
||||
opt: paths=source_relative,require_unimplemented_servers=false
|
||||
|
||||
- name: grpc-gateway
|
||||
out: gen/grpc/
|
||||
out: ../service/gen/grpc/
|
||||
opt: paths=source_relative
|
||||
|
||||
# - name: swagger
|
||||
8
proto/buf.lock
Normal file
8
proto/buf.lock
Normal file
@@ -0,0 +1,8 @@
|
||||
# Generated by buf. DO NOT EDIT.
|
||||
version: v1
|
||||
deps:
|
||||
- remote: buf.build
|
||||
owner: googleapis
|
||||
repository: googleapis
|
||||
commit: 751cbe31638d43a9bfb6162cd2352e67
|
||||
digest: shake256:87f55470d9d124e2d1dedfe0231221f4ed7efbc55bc5268917c678e2d9b9c41573a7f9a557f6d8539044524d9fc5ca8fbb7db05eb81379d168285d76b57eb8a4
|
||||
@@ -3,8 +3,7 @@ deps:
|
||||
- buf.build/googleapis/googleapis
|
||||
lint:
|
||||
use:
|
||||
- DEFAULT
|
||||
build:
|
||||
- STANDARD
|
||||
breaking:
|
||||
use:
|
||||
- FILE
|
||||
@@ -1,6 +1,8 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option go_package = "gen/grpc";
|
||||
package olivetin.api.v1;
|
||||
|
||||
option go_package = "github.com/jamesread/OliveTin/gen/grpc/olivetin/api/v1;apiv1";
|
||||
|
||||
import "google/api/annotations.proto";
|
||||
import "google/api/httpbody.proto";
|
||||
@@ -46,6 +48,7 @@ message GetDashboardComponentsResponse {
|
||||
repeated DashboardComponent dashboards = 4;
|
||||
|
||||
string authenticated_user = 5;
|
||||
string authenticated_user_provider = 6;
|
||||
}
|
||||
|
||||
message GetDashboardComponentsRequest {}
|
||||
@@ -55,6 +58,7 @@ message DashboardComponent {
|
||||
string type = 2;
|
||||
repeated DashboardComponent contents = 3;
|
||||
string icon = 4;
|
||||
string css_class = 5;
|
||||
}
|
||||
|
||||
message StartActionRequest {
|
||||
@@ -76,6 +80,8 @@ message StartActionResponse {
|
||||
|
||||
message StartActionAndWaitRequest {
|
||||
string action_id = 1;
|
||||
|
||||
repeated StartActionArgument arguments = 2;
|
||||
}
|
||||
|
||||
message StartActionAndWaitResponse {
|
||||
@@ -98,7 +104,9 @@ message StartActionByGetAndWaitResponse {
|
||||
LogEntry log_entry = 1;
|
||||
}
|
||||
|
||||
message GetLogsRequest{};
|
||||
message GetLogsRequest{
|
||||
int64 start_offset = 1;
|
||||
};
|
||||
|
||||
message LogEntry {
|
||||
string datetime_started = 1;
|
||||
@@ -116,10 +124,13 @@ message LogEntry {
|
||||
bool execution_started = 14;
|
||||
bool execution_finished = 15;
|
||||
bool blocked = 16;
|
||||
int64 datetime_index = 17;
|
||||
}
|
||||
|
||||
message GetLogsResponse {
|
||||
repeated LogEntry logs = 1;
|
||||
int64 count_remaining = 2;
|
||||
int64 page_size = 3;
|
||||
}
|
||||
|
||||
message ValidateArgumentTypeRequest {
|
||||
@@ -153,6 +164,12 @@ message WhoAmIRequest {}
|
||||
|
||||
message WhoAmIResponse {
|
||||
string authenticated_user = 1;
|
||||
string usergroup = 2;
|
||||
string provider = 3;
|
||||
|
||||
repeated string acls = 4;
|
||||
|
||||
string sid = 5;
|
||||
}
|
||||
|
||||
message SosReportRequest {}
|
||||
@@ -197,6 +214,10 @@ message EventExecutionFinished {
|
||||
LogEntry log_entry = 1;
|
||||
}
|
||||
|
||||
message EventExecutionStarted {
|
||||
LogEntry log_entry = 1;
|
||||
}
|
||||
|
||||
message KillActionRequest {
|
||||
string execution_tracking_id = 1;
|
||||
}
|
||||
@@ -208,6 +229,24 @@ message KillActionResponse {
|
||||
bool found = 4;
|
||||
}
|
||||
|
||||
message LocalUserLoginRequest {
|
||||
string username = 1;
|
||||
string password = 2;
|
||||
}
|
||||
|
||||
message LocalUserLoginResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message PasswordHashRequest {
|
||||
string password = 1;
|
||||
}
|
||||
|
||||
message PasswordHashResponse {
|
||||
}
|
||||
|
||||
message LogoutRequest {}
|
||||
|
||||
service OliveTinApiService {
|
||||
rpc GetDashboardComponents(GetDashboardComponentsRequest) returns (GetDashboardComponentsResponse) {
|
||||
option (google.api.http) = {
|
||||
@@ -297,4 +336,24 @@ service OliveTinApiService {
|
||||
get: "/api/readyz"
|
||||
};
|
||||
}
|
||||
|
||||
rpc LocalUserLogin(LocalUserLoginRequest) returns (LocalUserLoginResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/LocalUserLogin"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
rpc PasswordHash(PasswordHashRequest) returns (google.api.HttpBody) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/PasswordHash"
|
||||
body: "*"
|
||||
};
|
||||
}
|
||||
|
||||
rpc Logout(LogoutRequest) returns (google.api.HttpBody) {
|
||||
option (google.api.http) = {
|
||||
get: "/api/Logout"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ tmp_dir = "tmp"
|
||||
[build]
|
||||
args_bin = []
|
||||
bin = "./OliveTin"
|
||||
cmd = "go build -o OliveTin github.com/OliveTin/OliveTin/cmd/OliveTin"
|
||||
cmd = "go build -o OliveTin"
|
||||
delay = 1
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata", "webui.dev", "webui"]
|
||||
exclude_file = []
|
||||
48
service/Makefile
Normal file
48
service/Makefile
Normal file
@@ -0,0 +1,48 @@
|
||||
define delete-files
|
||||
python -c "import shutil;shutil.rmtree('$(1)', ignore_errors=True)"
|
||||
endef
|
||||
|
||||
compile-currentenv:
|
||||
go build
|
||||
|
||||
prep:
|
||||
go mod download
|
||||
go generate ./...
|
||||
|
||||
compile-armhf:
|
||||
go env -w GOARCH=arm GOARM=6
|
||||
go build -o OliveTin.armhf
|
||||
go env -u GOARCH GOARM
|
||||
|
||||
compile-x64-lin:
|
||||
go env -w GOOS=linux
|
||||
go build -o OliveTin
|
||||
go env -u GOOS
|
||||
|
||||
compile-x64-win:
|
||||
go env -w GOOS=windows GOARCH=amd64
|
||||
go build -o OliveTin.exe
|
||||
go env -u GOOS GOARCH
|
||||
|
||||
compile: compile-armhf compile-x64-lin compile-x64-win
|
||||
|
||||
codestyle:
|
||||
go fmt ./...
|
||||
go vet ./...
|
||||
gocyclo -over 4 internal
|
||||
gocritic check ./...
|
||||
|
||||
unittests:
|
||||
$(call delete-files,reports)
|
||||
mkdir reports
|
||||
go test ./... -coverprofile reports/unittests.out
|
||||
go tool cover -html=reports/unittests.out -o reports/unittests.html
|
||||
|
||||
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"
|
||||
3
service/generate.go
Normal file
3
service/generate.go
Normal file
@@ -0,0 +1,3 @@
|
||||
//go:generate make -wC ../
|
||||
|
||||
package main
|
||||
165
service/go.mod
Normal file
165
service/go.mod
Normal file
@@ -0,0 +1,165 @@
|
||||
module github.com/OliveTin/OliveTin
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.23.7
|
||||
|
||||
require (
|
||||
github.com/MicahParks/keyfunc/v3 v3.3.2
|
||||
github.com/alexedwards/argon2id v1.0.0
|
||||
github.com/bufbuild/buf v1.50.1
|
||||
github.com/fsnotify/fsnotify v1.6.0
|
||||
github.com/fzipp/gocyclo v0.6.0
|
||||
github.com/go-critic/go-critic v0.13.0
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.4.1
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3
|
||||
github.com/prometheus/client_golang v1.20.2
|
||||
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.10.0
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
|
||||
golang.org/x/oauth2 v0.27.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4
|
||||
google.golang.org/grpc v1.71.0
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1
|
||||
google.golang.org/protobuf v1.36.5
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.4-20250121211742-6d880cc6cc8d.1 // indirect
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.4-20241127180247-a33202765966.1 // indirect
|
||||
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250116203702-1c024d64352b.1 // indirect
|
||||
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.4-20250116203702-1c024d64352b.1 // indirect
|
||||
buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.4-20241007202033-cf42259fcbfc.1 // indirect
|
||||
buf.build/go/bufplugin v0.7.0 // indirect
|
||||
buf.build/go/protoyaml v0.3.1 // indirect
|
||||
buf.build/go/spdx v0.2.0 // indirect
|
||||
cel.dev/expr v0.19.2 // indirect
|
||||
connectrpc.com/connect v1.18.1 // indirect
|
||||
connectrpc.com/otelconnect v0.7.1 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/MicahParks/jwkset v0.7.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bufbuild/protocompile v0.14.1 // indirect
|
||||
github.com/bufbuild/protoplugin v0.0.0-20250106231243-3a819552c9d9 // indirect
|
||||
github.com/bufbuild/protovalidate-go v0.8.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
|
||||
github.com/cristalhq/acmd v0.12.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/cli v27.5.1+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker v28.0.0+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.8.2 // 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.5 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.1 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // 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/flock v0.12.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/cel-go v0.24.1 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/go-containerregistry v0.20.3 // indirect
|
||||
github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 // 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.18.0 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // 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/locker v1.0.1 // indirect
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
github.com/moby/sys/mount v0.3.4 // indirect
|
||||
github.com/moby/sys/mountinfo v0.7.2 // indirect
|
||||
github.com/moby/sys/reexec v0.1.0 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
github.com/moby/sys/user v0.3.0 // indirect
|
||||
github.com/moby/sys/userns v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.22.2 // 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.6.1 // indirect
|
||||
github.com/prometheus/common v0.55.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/quasilyte/go-ruleguard v0.4.4 // 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/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.50.0 // indirect
|
||||
github.com/rs/cors v1.11.1 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/segmentio/encoding v0.4.1 // indirect
|
||||
github.com/spf13/afero v1.9.3 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/cobra v1.9.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.0 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
github.com/tetratelabs/wazero v1.9.0 // indirect
|
||||
github.com/vbatts/tar-split v0.12.1 // indirect
|
||||
go.lsp.dev/jsonrpc2 v0.10.0 // indirect
|
||||
go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 // indirect
|
||||
go.lsp.dev/protocol v0.12.0 // indirect
|
||||
go.lsp.dev/uri v0.3.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
go.uber.org/zap/exp v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/net v0.37.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/term v0.30.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
golang.org/x/tools v0.31.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
pluginrpc.com/pluginrpc v0.5.0 // indirect
|
||||
)
|
||||
@@ -1,5 +1,21 @@
|
||||
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=
|
||||
buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.4-20250121211742-6d880cc6cc8d.1 h1:p5SFT60M93aMQhOz81VH3kPg8t1pp/Litae/1eSxie4=
|
||||
buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.4-20250121211742-6d880cc6cc8d.1/go.mod h1:umI0o7WWHv8lCbLjYUMzfjHKjyaIt2D89sIj1D9fqy0=
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.4-20241127180247-a33202765966.1 h1:yeaeyw0RQUe009ebxBQ3TsqBPptiNEGsiS10t+8Htuo=
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.4-20241127180247-a33202765966.1/go.mod h1:novQBstnxcGpfKf8qGRATqn1anQKwMJIbH5Q581jibU=
|
||||
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250116203702-1c024d64352b.1 h1:1SDs5tEGoWWv2vmKLx2B0Bp+yfhlxiU4DaZUII8+Pvs=
|
||||
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250116203702-1c024d64352b.1/go.mod h1:o2AgVM1j3MczvxnMqfZTpiqGwK1VD4JbEagseY0QcjE=
|
||||
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.4-20250116203702-1c024d64352b.1 h1:uKJgSNHvwQUZ6+0dSnx9MtkZ+h/ORbkKym0rlzIjUSI=
|
||||
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.4-20250116203702-1c024d64352b.1/go.mod h1:Ua59W2s7uwPS5sGNgW08QewjBaPnUxOdpkWsuDvJ36Q=
|
||||
buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.4-20241007202033-cf42259fcbfc.1 h1:XmYgi9W/9oST2ZrfT3ucGWkzD9+Vd0ls9yhyZ8ae0KQ=
|
||||
buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.4-20241007202033-cf42259fcbfc.1/go.mod h1:cxFpqWIC80Wm8YNo1038ocBmrF84uQ0IfL0uVdAu9ZY=
|
||||
buf.build/go/bufplugin v0.7.0 h1:Tq8FXBVfpMxhl3QR6P/gMQHROg1Ss7WhpyD4QVV61ds=
|
||||
buf.build/go/bufplugin v0.7.0/go.mod h1:LuQzv36Ezu2zQIQUtwg4WJJFe58tXn1anL1IosAh6ik=
|
||||
buf.build/go/protoyaml v0.3.1 h1:ucyzE7DRnjX+mQ6AH4JzN0Kg50ByHHu+yrSKbgQn2D4=
|
||||
buf.build/go/protoyaml v0.3.1/go.mod h1:0TzNpFQDXhwbkXb/ajLvxIijqbve+vMQvWY/b3/Dzxg=
|
||||
buf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw=
|
||||
buf.build/go/spdx v0.2.0/go.mod h1:bXdwQFem9Si3nsbNy8aJKGPoaPi5DKwdeEp5/ArZ6w8=
|
||||
cel.dev/expr v0.19.2 h1:V354PbqIXr9IQdwy4SYA4xa0HXaWq1BUPAGzugBY5V4=
|
||||
cel.dev/expr v0.19.2/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
|
||||
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=
|
||||
@@ -37,38 +53,42 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
||||
connectrpc.com/connect v1.16.0 h1:rdtfQjZ0OyFkWPTegBNcH7cwquGAN1WzyJy80oFNibg=
|
||||
connectrpc.com/connect v1.16.0/go.mod h1:XpZAduBQUySsb4/KO5JffORVkDI4B6/EYPi7N8xpNZw=
|
||||
connectrpc.com/otelconnect v0.7.0 h1:ZH55ZZtcJOTKWWLy3qmL4Pam4RzRWBJFOqTPyAqCXkY=
|
||||
connectrpc.com/otelconnect v0.7.0/go.mod h1:Bt2ivBymHZHqxvo4HkJ0EwHuUzQN6k2l0oH+mp/8nwc=
|
||||
connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw=
|
||||
connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8=
|
||||
connectrpc.com/otelconnect v0.7.1 h1:scO5pOb0i4yUE66CnNrHeK1x51yq0bE0ehPg6WvzXJY=
|
||||
connectrpc.com/otelconnect v0.7.1/go.mod h1:dh3bFgHBTb2bkqGCeVVOtHJreSns7uu9wwL2Tbz17ms=
|
||||
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/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/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/jwkset v0.7.0 h1:CXWuiYBk5NuTl+N/3UI3UcYNH79yWuKAZWZkc/y+7Ok=
|
||||
github.com/MicahParks/jwkset v0.7.0/go.mod h1:fVrj6TmG1aKlJEeceAz7JsXGTXEn72zP1px3us53JrA=
|
||||
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/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
|
||||
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
||||
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/bufbuild/buf v1.50.1 h1:3sEaWLw6g7bSIJ+yKo6ERF3qpkaLNGd8SzImFpA5gUI=
|
||||
github.com/bufbuild/buf v1.50.1/go.mod h1:LqTlfsFs4RD3L+VoBudEWJzWi12Pa0+Q2vDQnY0YQv0=
|
||||
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
|
||||
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
|
||||
github.com/bufbuild/protoplugin v0.0.0-20250106231243-3a819552c9d9 h1:kAWER21DzhzU7ys8LL1WkSfbGkwXv+tM30hyEsYrW2k=
|
||||
github.com/bufbuild/protoplugin v0.0.0-20250106231243-3a819552c9d9/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ=
|
||||
github.com/bufbuild/protovalidate-go v0.8.2 h1:sgzXHkHYP6HnAsL2Rd3I1JxkYUyEQUv9awU1PduMxbM=
|
||||
github.com/bufbuild/protovalidate-go v0.8.2/go.mod h1:K6w8iPNAXBoIivVueSELbUeUl+MmeTQfCDSug85pn3M=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.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=
|
||||
@@ -82,30 +102,31 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
|
||||
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/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
|
||||
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
|
||||
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/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
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/cristalhq/acmd v0.12.0 h1:RdlKnxjN+txbQosg8p/TRNZ+J1Rdne43MVQZ1zDhGWk=
|
||||
github.com/cristalhq/acmd v0.12.0/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/cli v27.5.1+incompatible h1:JB9cieUT9YNiMITtIsguaN55PLOHhBSz3LKVc6cqWaY=
|
||||
github.com/docker/cli v27.5.1+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.0.2+incompatible h1:yGVmKUFGgcxA6PXWAokO0sQL22BrQ67cgVjko8tGdXE=
|
||||
github.com/docker/docker v26.0.2+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/docker v28.0.0+incompatible h1:Olh0KS820sJ7nPsBKChVhk5pzqcwDR15fumfAd/p9hM=
|
||||
github.com/docker/docker v28.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
|
||||
github.com/docker/docker-credential-helpers v0.8.2/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=
|
||||
@@ -116,11 +137,11 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/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/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
|
||||
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/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=
|
||||
github.com/felixge/fgprof v0.9.5/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=
|
||||
@@ -129,18 +150,20 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4
|
||||
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-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-critic/go-critic v0.13.0 h1:kJzM7wzltQasSUXtYyTl6UaPVySO6GkaR1thFnJ6afY=
|
||||
github.com/go-critic/go-critic v0.13.0/go.mod h1:M/YeuJ3vOCQDnP2SU+ZhjgRzwzcBW87JqLpMJLrZDLI=
|
||||
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/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/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-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
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=
|
||||
@@ -163,14 +186,12 @@ github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCs
|
||||
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/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
|
||||
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
|
||||
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/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/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=
|
||||
@@ -198,13 +219,12 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
|
||||
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/cel-go v0.24.1 h1:jsBCtxG8mM5wiUJDSGUqU0K7Mtr3w7Eyv00rw4DiZxI=
|
||||
github.com/google/cel-go v0.24.1/go.mod h1:Hdf9TqOaTNSFQA1ybQaRqATVoK7m/zcf7IMhGXP5zI8=
|
||||
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=
|
||||
@@ -214,12 +234,11 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
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/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI=
|
||||
github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI=
|
||||
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=
|
||||
@@ -235,8 +254,8 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe
|
||||
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/pprof v0.0.0-20250302191652-9094ed2288e7 h1:+J3r2e8+RsmN3vKfo75g0YSY61ms37qzPglu4p0sGro=
|
||||
github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
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=
|
||||
@@ -246,8 +265,8 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
|
||||
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/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||
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=
|
||||
@@ -260,15 +279,15 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
|
||||
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/jhump/protoreflect/v2 v2.0.0-beta.2 h1:qZU+rEZUOYTz1Bnhi3xbwn+VxdXkLVeEpAeZzVXLY88=
|
||||
github.com/jhump/protoreflect/v2 v2.0.0-beta.2/go.mod h1:4tnOYkB/mq7QTyS3YKtVtNrJv4Psqout8HA1U+hZtgM=
|
||||
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/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
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=
|
||||
@@ -279,24 +298,52 @@ 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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
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/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
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/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
|
||||
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
|
||||
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/sys/mount v0.3.4 h1:yn5jq4STPztkkzSKpZkLcmjue+bZJ0u2AuQY1iNI1Ww=
|
||||
github.com/moby/sys/mount v0.3.4/go.mod h1:KcQJMbQdJHPlq5lcYT+/CjatWM4PuxKe+XLSVS4J6Os=
|
||||
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
|
||||
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
|
||||
github.com/moby/sys/reexec v0.1.0 h1:RrBi8e0EBTLEgfruBOFcxtElzRGTEUkeIFaVXgU7wok=
|
||||
github.com/moby/sys/reexec v0.1.0/go.mod h1:EqjBg8F3X7iZe5pU6nRZnYCMUTXoxsjiIfHup5wYIN8=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
|
||||
github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
|
||||
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
|
||||
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
|
||||
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
|
||||
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/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU=
|
||||
github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec=
|
||||
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=
|
||||
@@ -309,44 +356,52 @@ github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDj
|
||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||
github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg=
|
||||
github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
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/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/quasilyte/go-ruleguard v0.4.4 h1:53DncefIeLX3qEpjzlS1lyUmQoUEeOWPFWqaTJq9eAQ=
|
||||
github.com/quasilyte/go-ruleguard v0.4.4/go.mod h1:Vl05zJ538vcEEwu16V/Hdu7IYZWyKSwIy4c88Ro1kRE=
|
||||
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/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.50.0 h1:3H/ld1pa3CYhkcc20TPIyG1bNsdhn9qZBGN3b9/UyUo=
|
||||
github.com/quic-go/quic-go v0.50.0/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E=
|
||||
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.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo=
|
||||
github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||
github.com/rs/cors v1.11.1/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/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/segmentio/encoding v0.4.1 h1:KLGaLSW0jrmhB58Nn4+98spfvPvmo4Ci1P/WIQ9wn7w=
|
||||
github.com/segmentio/encoding v0.4.1/go.mod h1:/d03Cd8PoaDeceuhUUUQWjU0KhWjrmYrWPgtJHYZSnI=
|
||||
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/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
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/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/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=
|
||||
@@ -361,57 +416,74 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.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/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
|
||||
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
|
||||
github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
|
||||
github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
|
||||
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=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.lsp.dev/jsonrpc2 v0.10.0 h1:Pr/YcXJoEOTMc/b6OTmcR1DPJ3mSWl/SWiU1Cct6VmI=
|
||||
go.lsp.dev/jsonrpc2 v0.10.0/go.mod h1:fmEzIdXPi/rf6d4uFcayi8HpFP1nBF99ERP1htC72Ac=
|
||||
go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 h1:hCzQgh6UcwbKgNSRurYWSqh8MufqRRPODRBblutn4TE=
|
||||
go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2/go.mod h1:gtSHRuYfbCT0qnbLnovpie/WEmqyJ7T4n6VXiFMBtcw=
|
||||
go.lsp.dev/protocol v0.12.0 h1:tNprUI9klQW5FAFVM4Sa+AbPFuVQByWhP1ttNUAjIWg=
|
||||
go.lsp.dev/protocol v0.12.0/go.mod h1:Qb11/HgZQ72qQbeyPfJbu3hZBH23s1sr4st8czGeDMQ=
|
||||
go.lsp.dev/uri v0.3.0 h1:KcZJmh6nFIBeJzTugn5JTU6OOyG0lDOo3R9KwTxTYbo=
|
||||
go.lsp.dev/uri v0.3.0/go.mod h1:P5sbO1IQR+qySTWOCnhnK7phBx+W3zbLqSMDJNTw88I=
|
||||
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.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
|
||||
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/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
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=
|
||||
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
|
||||
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
|
||||
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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
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/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
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=
|
||||
@@ -422,12 +494,12 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw=
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
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/exp/typeparams v0.0.0-20250305212735-054e65f0b394 h1:VI4qDpTkfFaCXEPrbojidLgVQhj2x4nzTccG0hjaLlU=
|
||||
golang.org/x/exp/typeparams v0.0.0-20250305212735-054e65f0b394/go.mod h1:LKZHyeOpPuZcMgxeHjJp4p5yvxrCX1xDvH10zYHhjjQ=
|
||||
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=
|
||||
@@ -451,8 +523,10 @@ 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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
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=
|
||||
@@ -484,8 +558,11 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
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=
|
||||
@@ -495,6 +572,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ
|
||||
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/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
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=
|
||||
@@ -505,8 +584,10 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
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=
|
||||
@@ -544,28 +625,41 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
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-20220520151302-bc2c85ada10a/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-20220722155257-8c9f86f7a55f/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.5.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/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
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/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||
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/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
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/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.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=
|
||||
@@ -615,8 +709,10 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
|
||||
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||
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=
|
||||
@@ -683,10 +779,10 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D
|
||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto/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/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 h1:IFnXJq3UPB3oBREOodn1v1aGQeZYQclEmvWRMN0PSsY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:c8q6Z6OCqnfVIqUFJkCzKcrj8eCvUrz+K4KRzSTuANg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
|
||||
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=
|
||||
@@ -703,10 +799,10 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.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/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
|
||||
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
|
||||
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=
|
||||
@@ -717,9 +813,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
|
||||
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=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
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=
|
||||
@@ -740,6 +835,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
||||
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=
|
||||
pluginrpc.com/pluginrpc v0.5.0 h1:tOQj2D35hOmvHyPu8e7ohW2/QvAnEtKscy2IJYWQ2yo=
|
||||
pluginrpc.com/pluginrpc v0.5.0/go.mod h1:UNWZ941hcVAoOZUn8YZsMmOZBzbUjQa3XMns8RQLp9o=
|
||||
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=
|
||||
241
service/internal/acl/acl.go
Normal file
241
service/internal/acl/acl.go
Normal file
@@ -0,0 +1,241 @@
|
||||
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
|
||||
|
||||
Provider string
|
||||
SID string
|
||||
|
||||
Acls []string
|
||||
}
|
||||
|
||||
func (u *AuthenticatedUser) IsGuest() bool {
|
||||
return u.Username == "guest" && u.Provider == "system"
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if cfg.LogDebugOptions.AclCheckStarted {
|
||||
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 {
|
||||
var ret *AuthenticatedUser
|
||||
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
|
||||
if ok {
|
||||
ret = &AuthenticatedUser{}
|
||||
ret.Username = getMetadataKeyOrEmpty(md, "username")
|
||||
ret.Usergroup = getMetadataKeyOrEmpty(md, "usergroup")
|
||||
ret.Provider = getMetadataKeyOrEmpty(md, "provider")
|
||||
|
||||
buildUserAcls(cfg, ret)
|
||||
}
|
||||
|
||||
if !ok || ret.Username == "" {
|
||||
ret = UserGuest(cfg)
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"username": ret.Username,
|
||||
"usergroup": ret.Usergroup,
|
||||
"provider": ret.Provider,
|
||||
"acls": ret.Acls,
|
||||
}).Debugf("UserFromContext")
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func UserGuest(cfg *config.Config) *AuthenticatedUser {
|
||||
ret := &AuthenticatedUser{}
|
||||
ret.Username = "guest"
|
||||
ret.Usergroup = "guest"
|
||||
ret.Provider = "system"
|
||||
|
||||
buildUserAcls(cfg, ret)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func UserFromSystem(cfg *config.Config, username string) *AuthenticatedUser {
|
||||
ret := &AuthenticatedUser{
|
||||
Username: username,
|
||||
Usergroup: "system",
|
||||
Provider: "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,5 +1,9 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Action represents the core functionality of OliveTin - commands that show up
|
||||
// as buttons in the UI.
|
||||
type Action struct {
|
||||
@@ -17,7 +21,7 @@ type Action struct {
|
||||
ExecOnFileCreatedInDir []string
|
||||
ExecOnFileChangedInDir []string
|
||||
ExecOnCalendarFile string
|
||||
Trigger string
|
||||
Triggers []string
|
||||
MaxConcurrent int
|
||||
MaxRate []RateSpec
|
||||
Arguments []ActionArgument
|
||||
@@ -83,6 +87,7 @@ type PrometheusConfig struct {
|
||||
type Config struct {
|
||||
UseSingleHTTPFrontend bool
|
||||
ThemeName string
|
||||
ThemeCacheDisabled bool
|
||||
ListenAddressSingleHTTPFrontend string
|
||||
ListenAddressWebUI string
|
||||
ListenAddressRestActions string
|
||||
@@ -91,6 +96,7 @@ type Config struct {
|
||||
ExternalRestAddress string
|
||||
LogLevel string
|
||||
LogDebugOptions LogDebugOptions
|
||||
LogHistoryPageSize int64
|
||||
Actions []*Action `mapstructure:"actions"`
|
||||
Entities []*EntityFile `mapstructure:"entities"`
|
||||
Dashboards []*DashboardComponent `mapstructure:"dashboards"`
|
||||
@@ -99,6 +105,7 @@ type Config struct {
|
||||
ShowFooter bool
|
||||
ShowNavigation bool
|
||||
ShowNewVersions bool
|
||||
EnableCustomJs bool
|
||||
AuthJwtCookieName string
|
||||
AuthJwtAud string
|
||||
AuthJwtDomain string
|
||||
@@ -109,6 +116,11 @@ type Config struct {
|
||||
AuthJwtPubKeyPath string // will read pub key from file on disk
|
||||
AuthHttpHeaderUsername string
|
||||
AuthHttpHeaderUserGroup string
|
||||
AuthLocalUsers AuthLocalUsersConfig
|
||||
AuthLoginUrl string
|
||||
AuthRequireGuestsToLogin bool
|
||||
AuthOAuth2RedirectURL string
|
||||
AuthOAuth2Providers map[string]*OAuth2Provider
|
||||
DefaultPermissions PermissionsList
|
||||
AccessControlLists []*AccessControlList
|
||||
WebUIDir string
|
||||
@@ -124,10 +136,45 @@ type Config struct {
|
||||
DefaultIconForActions string
|
||||
DefaultIconForDirectories string
|
||||
DefaultIconForBack string
|
||||
AdditionalNavigationLinks []*NavigationLink
|
||||
|
||||
usedConfigDir string
|
||||
}
|
||||
|
||||
type AuthLocalUsersConfig struct {
|
||||
Enabled bool
|
||||
Users []*LocalUser
|
||||
}
|
||||
|
||||
type LocalUser struct {
|
||||
Username string
|
||||
Usergroup string
|
||||
Password string
|
||||
}
|
||||
|
||||
type OAuth2Provider struct {
|
||||
Name string
|
||||
Title string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
Icon string
|
||||
Scopes []string
|
||||
AuthUrl string
|
||||
TokenUrl string
|
||||
WhoamiUrl string
|
||||
UsernameField string
|
||||
UserGroupField string
|
||||
InsecureSkipVerify bool
|
||||
CallbackTimeout int
|
||||
CertBundlePath string
|
||||
}
|
||||
|
||||
type NavigationLink struct {
|
||||
Title string
|
||||
Url string
|
||||
Target string
|
||||
}
|
||||
|
||||
type SaveLogsConfig struct {
|
||||
ResultsDirectory string
|
||||
OutputDirectory string
|
||||
@@ -136,8 +183,10 @@ type SaveLogsConfig struct {
|
||||
type LogDebugOptions struct {
|
||||
SingleFrontendRequests bool
|
||||
SingleFrontendRequestHeaders bool
|
||||
AclCheckStarted bool
|
||||
AclMatched bool
|
||||
AclNotMatched bool
|
||||
AclNoneMatched bool
|
||||
}
|
||||
|
||||
type DashboardComponent struct {
|
||||
@@ -145,30 +194,33 @@ type DashboardComponent struct {
|
||||
Type string
|
||||
Entity string
|
||||
Icon string
|
||||
CssClass string
|
||||
Contents []DashboardComponent
|
||||
}
|
||||
|
||||
// DefaultConfig gets a new Config structure with sensible default values.
|
||||
func DefaultConfig() *Config {
|
||||
return DefaultConfigWithBasePort(1337)
|
||||
}
|
||||
|
||||
// DefaultConfig gets a new Config structure with sensible default values.
|
||||
func DefaultConfigWithBasePort(basePort int) *Config {
|
||||
config := Config{}
|
||||
config.UseSingleHTTPFrontend = true
|
||||
config.PageTitle = "OliveTin"
|
||||
config.ShowFooter = true
|
||||
config.ShowNavigation = true
|
||||
config.ShowNewVersions = true
|
||||
config.ListenAddressSingleHTTPFrontend = "0.0.0.0:1337"
|
||||
config.ListenAddressRestActions = "localhost:1338"
|
||||
config.ListenAddressGrpcActions = "localhost:1339"
|
||||
config.ListenAddressWebUI = "localhost:1340"
|
||||
config.ListenAddressPrometheus = "localhost:1341"
|
||||
config.EnableCustomJs = false
|
||||
config.ExternalRestAddress = "."
|
||||
config.LogLevel = "INFO"
|
||||
config.CheckForUpdates = true
|
||||
config.LogHistoryPageSize = 10
|
||||
config.CheckForUpdates = false
|
||||
config.DefaultPermissions.Exec = true
|
||||
config.DefaultPermissions.View = true
|
||||
config.DefaultPermissions.Logs = true
|
||||
config.AuthJwtClaimUsername = "name"
|
||||
config.AuthJwtClaimUserGroup = "group"
|
||||
config.AuthRequireGuestsToLogin = false
|
||||
config.WebUIDir = "./webui"
|
||||
config.CronSupportForSeconds = false
|
||||
config.SectionNavigationStyle = "sidebar"
|
||||
@@ -182,6 +234,13 @@ func DefaultConfig() *Config {
|
||||
config.DefaultIconForActions = "😀"
|
||||
config.DefaultIconForDirectories = "📁"
|
||||
config.DefaultIconForBack = "«"
|
||||
config.ThemeCacheDisabled = false
|
||||
|
||||
config.ListenAddressSingleHTTPFrontend = fmt.Sprintf("0.0.0.0:%d", basePort)
|
||||
config.ListenAddressRestActions = fmt.Sprintf("localhost:%d", basePort+1)
|
||||
config.ListenAddressGrpcActions = fmt.Sprintf("localhost:%d", basePort+2)
|
||||
config.ListenAddressWebUI = fmt.Sprintf("localhost:%d", basePort+3)
|
||||
config.ListenAddressPrometheus = fmt.Sprintf("localhost:%d", basePort+4)
|
||||
|
||||
return &config
|
||||
}
|
||||
@@ -43,6 +43,16 @@ func (cfg *Config) FindAcl(aclTitle string) *AccessControlList {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Config) FindUserByUsername(searchUsername string) *LocalUser {
|
||||
for _, user := range cfg.AuthLocalUsers.Users {
|
||||
if user.Username == searchUsername {
|
||||
return user
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Config) SetDir(dir string) {
|
||||
cfg.usedConfigDir = dir
|
||||
}
|
||||
@@ -44,3 +44,10 @@ func TestFindAcl(t *testing.T) {
|
||||
assert.NotNil(t, c.FindAcl("Testing ACL"), "Find a ACL that should exist")
|
||||
assert.Nil(t, c.FindAcl("Chocolate Cake"), "Find a ACL that does not exist")
|
||||
}
|
||||
|
||||
func TestSetDir(t *testing.T) {
|
||||
c := DefaultConfig()
|
||||
c.SetDir("test")
|
||||
|
||||
assert.Equal(t, "test", c.GetDir(), "SetDir")
|
||||
}
|
||||
@@ -4,9 +4,10 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
@@ -14,7 +15,7 @@ import (
|
||||
var (
|
||||
metricConfigActionCount = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "olivetin_config_action_count",
|
||||
Help: "Then number of actions in the config file",
|
||||
Help: "The number of actions in the config file",
|
||||
})
|
||||
|
||||
metricConfigReloadedCount = promauto.NewCounter(prometheus.CounterOpts{
|
||||
@@ -38,7 +39,7 @@ func Reload(cfg *Config) {
|
||||
metricConfigReloadedCount.Inc()
|
||||
metricConfigActionCount.Set(float64(len(cfg.Actions)))
|
||||
|
||||
cfg.SetDir(path.Dir(viper.ConfigFileUsed()))
|
||||
cfg.SetDir(filepath.Dir(viper.ConfigFileUsed()))
|
||||
cfg.Sanitize()
|
||||
|
||||
for _, l := range listeners {
|
||||
@@ -14,6 +14,8 @@ var emojis = map[string]string{
|
||||
"logs": "🔍",
|
||||
"light": "💡",
|
||||
"robot": "🤖",
|
||||
"ssh": "🔐",
|
||||
"theme": "🎨",
|
||||
}
|
||||
|
||||
func lookupHTMLIcon(keyToLookup string, defaultIcon string) string {
|
||||
@@ -41,6 +41,28 @@ func (action *Action) sanitize(cfg *Config) {
|
||||
for idx := range action.Arguments {
|
||||
action.Arguments[idx].sanitize()
|
||||
}
|
||||
|
||||
sanitizeAuthRequireGuestsToLogin(cfg)
|
||||
sanitizeLogHistoryPageSize(cfg)
|
||||
}
|
||||
|
||||
func sanitizeAuthRequireGuestsToLogin(cfg *Config) {
|
||||
if cfg.AuthRequireGuestsToLogin {
|
||||
log.Infof("AuthRequireGuestsToLogin is enabled. All defaultPermissions will be set to false")
|
||||
|
||||
cfg.DefaultPermissions.View = false
|
||||
cfg.DefaultPermissions.Exec = false
|
||||
cfg.DefaultPermissions.Logs = false
|
||||
}
|
||||
}
|
||||
|
||||
func sanitizeLogHistoryPageSize(cfg *Config) {
|
||||
if cfg.LogHistoryPageSize < 10 {
|
||||
log.Warnf("LogsHistoryLimit is too low, setting it to 10")
|
||||
cfg.LogHistoryPageSize = 10
|
||||
} else if cfg.LogHistoryPageSize > 100 {
|
||||
log.Warnf("LogsHistoryLimit is high, you can do this, but expect browser lag.")
|
||||
}
|
||||
}
|
||||
|
||||
func getActionID(action *Action) string {
|
||||
@@ -55,10 +77,13 @@ func getActionID(action *Action) string {
|
||||
return action.ID
|
||||
}
|
||||
|
||||
//gocyclo:ignore
|
||||
func sanitizePopupOnStart(raw string, cfg *Config) string {
|
||||
switch raw {
|
||||
case "execution-dialog":
|
||||
return raw
|
||||
case "execution-dialog-output-html":
|
||||
return raw
|
||||
case "execution-dialog-stdout-only":
|
||||
return raw
|
||||
case "execution-button":
|
||||
@@ -32,7 +32,8 @@ func SetupEntityFileWatchers(cfg *config.Config) {
|
||||
configDir = configDirVar
|
||||
}
|
||||
|
||||
for _, ef := range cfg.Entities {
|
||||
for entityIndex, _ := range cfg.Entities { // #337 - iterate by key, not by value
|
||||
ef := cfg.Entities[entityIndex]
|
||||
p := ef.File
|
||||
|
||||
if !filepath.IsAbs(p) {
|
||||
@@ -72,12 +73,12 @@ func loadEntityFileJson(filename string, entityname string) {
|
||||
return
|
||||
}
|
||||
|
||||
data := make([]map[string]string, 0)
|
||||
data := make([]map[string]interface{}, 0)
|
||||
|
||||
decoder := json.NewDecoder(bytes.NewReader(jfile))
|
||||
|
||||
for decoder.More() {
|
||||
d := make(map[string]string)
|
||||
d := make(map[string]interface{})
|
||||
|
||||
err := decoder.Decode(&d)
|
||||
|
||||
@@ -105,7 +106,7 @@ func loadEntityFileYaml(filename string, entityname string) {
|
||||
return
|
||||
}
|
||||
|
||||
data := make([]map[string]string, 1)
|
||||
data := make([]map[string]interface{}, 1)
|
||||
|
||||
err = yaml.Unmarshal(yfile, &data)
|
||||
|
||||
@@ -116,7 +117,7 @@ func loadEntityFileYaml(filename string, entityname string) {
|
||||
updateSvFromFile(entityname, data)
|
||||
}
|
||||
|
||||
func updateSvFromFile(entityname string, data []map[string]string) {
|
||||
func updateSvFromFile(entityname string, data []map[string]interface{}) {
|
||||
log.Debugf("updateSvFromFile: %+v", data)
|
||||
|
||||
count := len(data)
|
||||
@@ -128,12 +129,34 @@ func updateSvFromFile(entityname string, data []map[string]string) {
|
||||
for i, mapp := range data {
|
||||
prefix := "entities." + entityname + "." + fmt.Sprintf("%v", i)
|
||||
|
||||
for k, v := range mapp {
|
||||
sv.Set(prefix+"."+k, v)
|
||||
}
|
||||
serializeValueToSv(prefix, mapp)
|
||||
}
|
||||
|
||||
for _, l := range listeners {
|
||||
l()
|
||||
}
|
||||
}
|
||||
|
||||
func serializeValueToSv(prefix string, value interface{}) {
|
||||
if m, ok := value.(map[string]interface{}); ok { // if value is a map we need to flatten it
|
||||
serializeMapToSv(prefix, m)
|
||||
} else if s, ok := value.([]interface{}); ok { // if value is a slice we need to flatten it
|
||||
serializeSliceToSv(prefix, s)
|
||||
} else {
|
||||
sv.Set(prefix, fmt.Sprintf("%v", value))
|
||||
}
|
||||
}
|
||||
|
||||
func serializeMapToSv(prefix string, m map[string]interface{}) {
|
||||
for k, v := range m {
|
||||
serializeValueToSv(prefix+"."+k, v)
|
||||
}
|
||||
}
|
||||
|
||||
func serializeSliceToSv(prefix string, s []interface{}) {
|
||||
sv.Set(prefix+".count", fmt.Sprintf("%v", len(s)))
|
||||
|
||||
for i, v := range s {
|
||||
serializeValueToSv(prefix+"."+fmt.Sprintf("%v", i), v)
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"errors"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -23,35 +24,51 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func parseCommandForReplacements(rawShellCommand string, values map[string]string) (string, map[string]string, error) {
|
||||
r := regexp.MustCompile("{{ *?([a-zA-Z0-9_]+?) *?}}")
|
||||
foundArgumentNames := r.FindAllStringSubmatch(rawShellCommand, -1)
|
||||
|
||||
usedArguments := make(map[string]string)
|
||||
|
||||
for _, match := range foundArgumentNames {
|
||||
argName := match[1]
|
||||
argValue, argProvided := values[argName]
|
||||
|
||||
if !argProvided {
|
||||
return "", nil, errors.New("Required arg not provided: " + argName)
|
||||
}
|
||||
|
||||
usedArguments[argName] = argValue
|
||||
|
||||
rawShellCommand = strings.ReplaceAll(rawShellCommand, match[0], argValue)
|
||||
}
|
||||
|
||||
return rawShellCommand, usedArguments, nil
|
||||
}
|
||||
|
||||
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)
|
||||
rawShellCommand, usedArgs, err := parseCommandForReplacements(rawShellCommand, values)
|
||||
|
||||
for _, match := range matches {
|
||||
argValue, argProvided := values[match[1]]
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !argProvided {
|
||||
log.Infof("%v", values)
|
||||
return "", errors.New("Required arg not provided: " + match[1])
|
||||
}
|
||||
|
||||
err := typecheckActionArgument(match[1], argValue, action)
|
||||
for argName, argValue := range usedArgs {
|
||||
err := typecheckActionArgument(argName, argValue, action)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"name": match[1],
|
||||
"name": argName,
|
||||
"value": argValue,
|
||||
}).Debugf("Arg assigned")
|
||||
|
||||
rawShellCommand = strings.ReplaceAll(rawShellCommand, match[0], argValue)
|
||||
}
|
||||
|
||||
rawShellCommand = sv.ReplaceEntityVars(entityPrefix, rawShellCommand)
|
||||
@@ -121,24 +138,47 @@ func typecheckChoiceEntity(value string, arg *config.ActionArgument) error {
|
||||
// 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.
|
||||
//
|
||||
//gocyclo:ignore
|
||||
func TypeSafetyCheck(name string, value string, argumentType string) error {
|
||||
if argumentType == "url" {
|
||||
return typeSafetyCheckUrl(name, value)
|
||||
}
|
||||
|
||||
if argumentType == "datetime" {
|
||||
_, err := time.Parse("2006-01-02T15:04:05", value)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch argumentType {
|
||||
case "password":
|
||||
return nil
|
||||
case "raw_string_multiline":
|
||||
return nil
|
||||
case "email":
|
||||
return typeSafetyCheckEmail(name, value)
|
||||
case "url":
|
||||
return typeSafetyCheckUrl(name, value)
|
||||
case "datetime":
|
||||
return typeSafetyCheckDatetime(name, value)
|
||||
}
|
||||
|
||||
return typeSafetyCheckRegex(name, value, argumentType)
|
||||
}
|
||||
|
||||
func typeSafetyCheckEmail(name string, value string) error {
|
||||
_, err := mail.ParseAddress(value)
|
||||
|
||||
log.Errorf("Email check: %v, %v", err, value)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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 := ""
|
||||
|
||||
@@ -15,16 +15,14 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
metricActionsRequested = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
metricActionsRequested = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "olivetin_actions_requested_count",
|
||||
Help: "The actions requested count",
|
||||
})
|
||||
@@ -39,8 +37,11 @@ type ActionBinding struct {
|
||||
// Executor represents a helper class for executing commands. It's main method
|
||||
// is ExecRequest
|
||||
type Executor struct {
|
||||
Logs map[string]*InternalLogEntry
|
||||
LogsByActionId map[string][]*InternalLogEntry
|
||||
logs map[string]*InternalLogEntry
|
||||
logsTrackingIdsByDate []string
|
||||
LogsByActionId map[string][]*InternalLogEntry
|
||||
|
||||
logmutex sync.RWMutex
|
||||
|
||||
MapActionIdToBinding map[string]*ActionBinding
|
||||
MapActionIdToBindingLock sync.RWMutex
|
||||
@@ -84,6 +85,8 @@ type InternalLogEntry struct {
|
||||
ExecutionFinished bool
|
||||
ExecutionTrackingID string
|
||||
Process *os.Process
|
||||
Username string
|
||||
Index int64
|
||||
|
||||
/*
|
||||
The following 3 properties are obviously on Action normally, but it's useful
|
||||
@@ -102,7 +105,8 @@ type executorStepFunc func(*ExecutionRequest) bool
|
||||
func DefaultExecutor(cfg *config.Config) *Executor {
|
||||
e := Executor{}
|
||||
e.Cfg = cfg
|
||||
e.Logs = make(map[string]*InternalLogEntry)
|
||||
e.logs = make(map[string]*InternalLogEntry)
|
||||
e.logsTrackingIdsByDate = make([]string, 0)
|
||||
e.LogsByActionId = make(map[string][]*InternalLogEntry)
|
||||
e.MapActionIdToBinding = make(map[string]*ActionBinding)
|
||||
|
||||
@@ -124,7 +128,7 @@ func DefaultExecutor(cfg *config.Config) *Executor {
|
||||
}
|
||||
|
||||
type listener interface {
|
||||
OnExecutionStarted(actionTitle string)
|
||||
OnExecutionStarted(logEntry *InternalLogEntry)
|
||||
OnExecutionFinished(logEntry *InternalLogEntry)
|
||||
OnOutputChunk(o []byte, executionTrackingId string)
|
||||
OnActionMapRebuilt()
|
||||
@@ -134,14 +138,106 @@ func (e *Executor) AddListener(m listener) {
|
||||
e.listeners = append(e.listeners, m)
|
||||
}
|
||||
|
||||
// getPagingStartIndex calculates the starting index for log pagination.
|
||||
// Parameters:
|
||||
//
|
||||
// startOffset: The offset from the most recent log (0 means start from the most recent)
|
||||
// totalLogCount: Total number of logs available
|
||||
// count: Number of logs to retrieve
|
||||
//
|
||||
// Returns: The calculated starting index for pagination
|
||||
func getPagingStartIndex(startOffset int64, totalLogCount int64, count int64) int64 {
|
||||
var startIndex int64
|
||||
|
||||
if startOffset <= 0 {
|
||||
startIndex = totalLogCount
|
||||
} else {
|
||||
startIndex = (totalLogCount - startOffset)
|
||||
|
||||
if startIndex < 0 {
|
||||
startIndex = 1
|
||||
}
|
||||
}
|
||||
|
||||
return startIndex - 1
|
||||
}
|
||||
|
||||
func (e *Executor) GetLogTrackingIds(startOffset int64, pageCount int64) ([]*InternalLogEntry, int64) {
|
||||
e.logmutex.RLock()
|
||||
|
||||
totalLogCount := int64(len(e.logsTrackingIdsByDate))
|
||||
|
||||
startIndex := getPagingStartIndex(startOffset, totalLogCount, pageCount)
|
||||
|
||||
pageCount = min(totalLogCount, pageCount)
|
||||
|
||||
endIndex := max(0, (startIndex-pageCount)+1)
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"startOffset": startOffset,
|
||||
"pageCount": pageCount,
|
||||
"total": totalLogCount,
|
||||
"startIndex": startIndex,
|
||||
"endIndex": endIndex,
|
||||
}).Tracef("GetLogTrackingIds")
|
||||
|
||||
trackingIds := make([]*InternalLogEntry, 0, pageCount)
|
||||
|
||||
if totalLogCount > 0 {
|
||||
for i := endIndex; i <= startIndex; i++ {
|
||||
trackingIds = append(trackingIds, e.logs[e.logsTrackingIdsByDate[i]])
|
||||
}
|
||||
}
|
||||
|
||||
e.logmutex.RUnlock()
|
||||
|
||||
remainingLogs := endIndex
|
||||
|
||||
return trackingIds, remainingLogs
|
||||
}
|
||||
|
||||
func (e *Executor) GetLog(trackingID string) (*InternalLogEntry, bool) {
|
||||
e.logmutex.RLock()
|
||||
|
||||
entry, found := e.logs[trackingID]
|
||||
|
||||
e.logmutex.RUnlock()
|
||||
|
||||
return entry, found
|
||||
}
|
||||
|
||||
func (e *Executor) GetLogsByActionId(actionId string) []*InternalLogEntry {
|
||||
e.logmutex.RLock()
|
||||
|
||||
logs, found := e.LogsByActionId[actionId]
|
||||
|
||||
e.logmutex.RUnlock()
|
||||
|
||||
if !found {
|
||||
return make([]*InternalLogEntry, 0)
|
||||
}
|
||||
|
||||
return logs
|
||||
}
|
||||
|
||||
func (e *Executor) SetLog(trackingID string, entry *InternalLogEntry) {
|
||||
e.logmutex.Lock()
|
||||
|
||||
entry.Index = int64(len(e.logsTrackingIdsByDate))
|
||||
|
||||
e.logs[trackingID] = entry
|
||||
e.logsTrackingIdsByDate = append(e.logsTrackingIdsByDate, trackingID)
|
||||
|
||||
e.logmutex.Unlock()
|
||||
}
|
||||
|
||||
// ExecRequest processes an ExecutionRequest
|
||||
func (e *Executor) ExecRequest(req *ExecutionRequest) (*sync.WaitGroup, string) {
|
||||
if req.AuthenticatedUser == nil {
|
||||
req.AuthenticatedUser = acl.UserGuest(req.Cfg)
|
||||
}
|
||||
|
||||
req.executor = e
|
||||
|
||||
// req.UUID is now set by the client, so that they can track the request
|
||||
// from start to finish. This means that a malicious client could send
|
||||
// duplicate UUIDs (or just random strings), but this is the only way.
|
||||
|
||||
req.logEntry = &InternalLogEntry{
|
||||
DatetimeStarted: time.Now(),
|
||||
ExecutionTrackingID: req.TrackingID,
|
||||
@@ -152,15 +248,18 @@ func (e *Executor) ExecRequest(req *ExecutionRequest) (*sync.WaitGroup, string)
|
||||
ActionId: "",
|
||||
ActionTitle: "notfound",
|
||||
ActionIcon: "💩",
|
||||
Username: req.AuthenticatedUser.Username,
|
||||
}
|
||||
|
||||
_, foundLog := e.Logs[req.TrackingID]
|
||||
_, isDuplicate := e.GetLog(req.TrackingID)
|
||||
|
||||
if foundLog || req.TrackingID == "" {
|
||||
if isDuplicate || req.TrackingID == "" {
|
||||
req.TrackingID = uuid.NewString()
|
||||
}
|
||||
|
||||
e.Logs[req.TrackingID] = req.logEntry
|
||||
log.Tracef("executor.ExecRequest(): %v", req)
|
||||
|
||||
e.SetLog(req.TrackingID, req.logEntry)
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
wg.Add(1)
|
||||
@@ -184,18 +283,22 @@ func (e *Executor) execChain(req *ExecutionRequest) {
|
||||
|
||||
// This isn't a step, because we want to notify all listeners, irrespective
|
||||
// of how many steps were actually executed.
|
||||
notifyListeners(req)
|
||||
notifyListenersFinished(req)
|
||||
}
|
||||
|
||||
func getConcurrentCount(req *ExecutionRequest) int {
|
||||
concurrentCount := 0
|
||||
|
||||
for _, log := range req.executor.LogsByActionId[req.Action.ID] {
|
||||
req.executor.logmutex.RLock()
|
||||
|
||||
for _, log := range req.executor.GetLogsByActionId(req.Action.ID) {
|
||||
if !log.ExecutionFinished {
|
||||
concurrentCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
req.executor.logmutex.RUnlock()
|
||||
|
||||
return concurrentCount
|
||||
}
|
||||
|
||||
@@ -237,7 +340,7 @@ func getExecutionsCount(rate config.RateSpec, req *ExecutionRequest) int {
|
||||
|
||||
then := time.Now().Add(-duration)
|
||||
|
||||
for _, logEntry := range req.executor.LogsByActionId[req.Action.ID] {
|
||||
for _, logEntry := range req.executor.GetLogsByActionId(req.Action.ID) {
|
||||
if logEntry.DatetimeStarted.After(then) && !logEntry.Blocked {
|
||||
|
||||
executions += 1
|
||||
@@ -268,12 +371,30 @@ func stepRateCheck(req *ExecutionRequest) bool {
|
||||
}
|
||||
|
||||
func stepACLCheck(req *ExecutionRequest) bool {
|
||||
return acl.IsAllowedExec(req.Cfg, req.AuthenticatedUser, req.Action)
|
||||
canExec := acl.IsAllowedExec(req.Cfg, req.AuthenticatedUser, req.Action)
|
||||
|
||||
if !canExec {
|
||||
req.logEntry.Output = "ACL check failed. Blocked from executing."
|
||||
req.logEntry.Blocked = true
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"actionTitle": req.logEntry.ActionTitle,
|
||||
}).Warnf("ACL check failed. Blocked from executing.")
|
||||
}
|
||||
|
||||
return canExec
|
||||
}
|
||||
|
||||
func stepParseArgs(req *ExecutionRequest) bool {
|
||||
var err error
|
||||
|
||||
if req.Arguments == nil {
|
||||
req.Arguments = make(map[string]string)
|
||||
}
|
||||
|
||||
req.Arguments["ot_executionTrackingId"] = req.TrackingID
|
||||
req.Arguments["ot_username"] = req.AuthenticatedUser.Username
|
||||
|
||||
req.finalParsedCommand, err = parseActionArguments(req.Action.Shell, req.Arguments, req.Action, req.logEntry.ActionTitle, req.EntityPrefix)
|
||||
|
||||
if err != nil {
|
||||
@@ -312,6 +433,9 @@ func stepRequestAction(req *ExecutionRequest) bool {
|
||||
req.logEntry.ActionTitle = sv.ReplaceEntityVars(req.EntityPrefix, req.Action.Title)
|
||||
req.logEntry.ActionIcon = req.Action.Icon
|
||||
req.logEntry.ActionId = req.Action.ID
|
||||
req.logEntry.Tags = req.Tags
|
||||
|
||||
req.executor.logmutex.Lock()
|
||||
|
||||
if _, containsKey := req.executor.LogsByActionId[req.Action.ID]; !containsKey {
|
||||
req.executor.LogsByActionId[req.Action.ID] = make([]*InternalLogEntry, 0)
|
||||
@@ -319,11 +443,15 @@ func stepRequestAction(req *ExecutionRequest) bool {
|
||||
|
||||
req.executor.LogsByActionId[req.Action.ID] = append(req.executor.LogsByActionId[req.Action.ID], req.logEntry)
|
||||
|
||||
req.executor.logmutex.Unlock()
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"actionTitle": req.logEntry.ActionTitle,
|
||||
"tags": req.Tags,
|
||||
}).Infof("Action requested")
|
||||
|
||||
notifyListenersStarted(req)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -331,7 +459,7 @@ func stepLogStart(req *ExecutionRequest) bool {
|
||||
log.WithFields(log.Fields{
|
||||
"actionTitle": req.logEntry.ActionTitle,
|
||||
"timeout": req.Action.Timeout,
|
||||
}).Infof("Action starting")
|
||||
}).Infof("Action started")
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -349,18 +477,16 @@ func stepLogFinish(req *ExecutionRequest) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func notifyListeners(req *ExecutionRequest) {
|
||||
func notifyListenersFinished(req *ExecutionRequest) {
|
||||
for _, listener := range req.executor.listeners {
|
||||
listener.OnExecutionFinished(req.logEntry)
|
||||
}
|
||||
}
|
||||
|
||||
func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cmd {
|
||||
if runtime.GOOS == "windows" {
|
||||
return exec.CommandContext(ctx, "cmd", "/C", finalParsedCommand)
|
||||
func notifyListenersStarted(req *ExecutionRequest) {
|
||||
for _, listener := range req.executor.listeners {
|
||||
listener.OnExecutionStarted(req.logEntry)
|
||||
}
|
||||
|
||||
return exec.CommandContext(ctx, "sh", "-c", finalParsedCommand)
|
||||
}
|
||||
|
||||
func appendErrorToStderr(err error, logEntry *InternalLogEntry) {
|
||||
@@ -386,11 +512,18 @@ func (ost *OutputStreamer) String() string {
|
||||
return ost.output.String()
|
||||
}
|
||||
|
||||
func buildEnv(req *ExecutionRequest) []string {
|
||||
func buildEnv(args map[string]string) []string {
|
||||
ret := append(os.Environ(), "OLIVETIN=1")
|
||||
|
||||
for k, v := range req.Arguments {
|
||||
ret = append(ret, fmt.Sprintf("%v=%v", strings.ToUpper(k), v))
|
||||
for k, v := range args {
|
||||
varName := fmt.Sprintf("%v", strings.TrimSpace(strings.ToUpper(k)))
|
||||
|
||||
// Skip arguments that might not have a name (eg, confirmation), as this causes weird bugs on Windows.
|
||||
if varName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
ret = append(ret, fmt.Sprintf("%v=%v", varName, v))
|
||||
}
|
||||
|
||||
return ret
|
||||
@@ -405,7 +538,7 @@ func stepExec(req *ExecutionRequest) bool {
|
||||
cmd := wrapCommandInShell(ctx, req.finalParsedCommand)
|
||||
cmd.Stdout = streamer
|
||||
cmd.Stderr = streamer
|
||||
cmd.Env = buildEnv(req)
|
||||
cmd.Env = buildEnv(req.Arguments)
|
||||
|
||||
req.logEntry.ExecutionStarted = true
|
||||
|
||||
@@ -422,10 +555,16 @@ func stepExec(req *ExecutionRequest) bool {
|
||||
appendErrorToStderr(waiterr, req.logEntry)
|
||||
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
log.WithFields(log.Fields{
|
||||
"actionTitle": req.logEntry.ActionTitle,
|
||||
}).Warnf("Action timed out")
|
||||
|
||||
// The context timeout should kill the process, but let's make sure.
|
||||
req.executor.Kill(req.logEntry)
|
||||
req.logEntry.TimedOut = true
|
||||
req.logEntry.Output += "OliveTin::timeout - this action timed out after " + fmt.Sprintf("%v", req.Action.Timeout) + " seconds. If you need more time for this action, set a longer timeout. See https://docs.olivetin.app/timeout.html for more help."
|
||||
}
|
||||
|
||||
req.logEntry.Tags = req.Tags
|
||||
req.logEntry.DatetimeFinished = time.Now()
|
||||
|
||||
return true
|
||||
@@ -443,39 +582,71 @@ func stepExecAfter(req *ExecutionRequest) bool {
|
||||
var stderr bytes.Buffer
|
||||
|
||||
args := map[string]string{
|
||||
"output": req.logEntry.Output,
|
||||
"exitCode": fmt.Sprintf("%v", req.logEntry.ExitCode),
|
||||
"output": req.logEntry.Output,
|
||||
"exitCode": fmt.Sprintf("%v", req.logEntry.ExitCode),
|
||||
"ot_executionTrackingId": req.TrackingID,
|
||||
"ot_username": req.AuthenticatedUser.Username,
|
||||
}
|
||||
|
||||
finalParsedCommand, _ := parseActionArguments(req.Action.ShellAfterCompleted, args, req.Action, req.logEntry.ActionTitle, req.EntityPrefix)
|
||||
finalParsedCommand, _, err := parseCommandForReplacements(req.Action.ShellAfterCompleted, args)
|
||||
|
||||
if err != nil {
|
||||
msg := "Could not prepare shellAfterCompleted command: " + err.Error() + "\n"
|
||||
req.logEntry.Output += msg
|
||||
log.Warnf(msg)
|
||||
return true
|
||||
}
|
||||
|
||||
cmd := wrapCommandInShell(ctx, finalParsedCommand)
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
cmd.Env = buildEnv(args)
|
||||
|
||||
runerr := cmd.Start()
|
||||
|
||||
waiterr := cmd.Wait()
|
||||
|
||||
req.logEntry.Output += "---\n" + stdout.String()
|
||||
req.logEntry.Output += "---\n" + stderr.String()
|
||||
req.logEntry.Output += "\n"
|
||||
req.logEntry.Output += "OliveTin::shellAfterCompleted stdout\n"
|
||||
req.logEntry.Output += stdout.String()
|
||||
|
||||
req.logEntry.Output += "OliveTin::shellAfterCompleted stderr\n"
|
||||
req.logEntry.Output += stderr.String()
|
||||
|
||||
req.logEntry.Output += "OliveTin::shellAfterCompleted errors and summary\n"
|
||||
appendErrorToStderr(runerr, req.logEntry)
|
||||
appendErrorToStderr(waiterr, req.logEntry)
|
||||
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
req.logEntry.Output += "Your shellAfterCommand command timed out."
|
||||
req.logEntry.Output += "Your shellAfterCompleted command timed out."
|
||||
}
|
||||
|
||||
req.logEntry.Output += fmt.Sprintf("Your shellAfterCommand exited with code %v", cmd.ProcessState.ExitCode())
|
||||
req.logEntry.Output += fmt.Sprintf("Your shellAfterCompleted exited with code %v\n", cmd.ProcessState.ExitCode())
|
||||
|
||||
req.logEntry.Output += "OliveTin::shellAfterCompleted output complete\n"
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func stepTrigger(req *ExecutionRequest) bool {
|
||||
if req.Action.Trigger != "" {
|
||||
if req.Action.Triggers == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(req.Tags) > 0 && req.Tags[0] == "trigger" {
|
||||
log.Warnf("Trigger action is triggering another trigger action. This is allowed, but be careful not to create trigger loops.")
|
||||
}
|
||||
|
||||
triggerLoop(req)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func triggerLoop(req *ExecutionRequest) {
|
||||
for _, triggerReq := range req.Action.Triggers {
|
||||
trigger := &ExecutionRequest{
|
||||
ActionTitle: req.Action.Trigger,
|
||||
ActionTitle: triggerReq,
|
||||
TrackingID: uuid.NewString(),
|
||||
Tags: []string{"trigger"},
|
||||
AuthenticatedUser: req.AuthenticatedUser,
|
||||
@@ -484,8 +655,6 @@ func stepTrigger(req *ExecutionRequest) bool {
|
||||
|
||||
req.executor.ExecRequest(trigger)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func stepSaveLog(req *ExecutionRequest) bool {
|
||||
@@ -109,3 +109,73 @@ func TestArgumentNameSnakeCase(t *testing.T) {
|
||||
assert.Equal(t, "echo 'Tickling Fred'", out)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestGetLogsEmpty(t *testing.T) {
|
||||
e, cfg := testingExecutor()
|
||||
|
||||
assert.Equal(t, int64(10), cfg.LogHistoryPageSize, "Logs page size should be 10")
|
||||
|
||||
logs, remaining := e.GetLogTrackingIds(0, 10)
|
||||
|
||||
assert.NotNil(t, logs, "Logs should not be nil")
|
||||
assert.Equal(t, 0, len(logs), "No logs yet")
|
||||
assert.Equal(t, int64(0), remaining, "There should be no remaining logs")
|
||||
}
|
||||
|
||||
func TestGetLogsLessThanPageSize(t *testing.T) {
|
||||
e, cfg := testingExecutor()
|
||||
|
||||
cfg.Actions = append(cfg.Actions, &config.Action{
|
||||
Title: "blat",
|
||||
Shell: "date",
|
||||
})
|
||||
|
||||
assert.Equal(t, int64(10), cfg.LogHistoryPageSize, "Logs page size should be 10")
|
||||
|
||||
logEntries, remaining := e.GetLogTrackingIds(0, 10)
|
||||
|
||||
assert.Equal(t, 0, len(logEntries), "There should be 0 logs")
|
||||
assert.Zero(t, remaining, "There should be no remaining logs")
|
||||
|
||||
execNewReqAndWait(e, "blat", cfg)
|
||||
execNewReqAndWait(e, "blat", cfg)
|
||||
execNewReqAndWait(e, "blat", cfg)
|
||||
execNewReqAndWait(e, "blat", cfg)
|
||||
execNewReqAndWait(e, "blat", cfg)
|
||||
execNewReqAndWait(e, "blat", cfg)
|
||||
execNewReqAndWait(e, "blat", cfg)
|
||||
|
||||
logEntries, remaining = e.GetLogTrackingIds(0, 10)
|
||||
|
||||
assert.Equal(t, 7, len(logEntries), "There should be 7 logs")
|
||||
assert.Zero(t, remaining, "There should be no remaining logs")
|
||||
|
||||
execNewReqAndWait(e, "blat", cfg)
|
||||
execNewReqAndWait(e, "blat", cfg)
|
||||
execNewReqAndWait(e, "blat", cfg)
|
||||
execNewReqAndWait(e, "blat", cfg)
|
||||
execNewReqAndWait(e, "blat", cfg)
|
||||
|
||||
logEntries, remaining = e.GetLogTrackingIds(0, 10)
|
||||
|
||||
assert.Equal(t, 10, len(logEntries), "There should be 10 logs")
|
||||
assert.Equal(t, int64(2), remaining, "There should be 1 remaining logs")
|
||||
}
|
||||
|
||||
func execNewReqAndWait(e *Executor, title string, cfg *config.Config) {
|
||||
req := &ExecutionRequest{
|
||||
ActionTitle: title,
|
||||
Cfg: cfg,
|
||||
}
|
||||
|
||||
wg, _ := e.ExecRequest(req)
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestGetPagingIndexes(t *testing.T) {
|
||||
assert.Zero(t, getPagingStartIndex(5, 0, 5), "Testing start index from empty list")
|
||||
assert.Equal(t, int64(4), getPagingStartIndex(5, 10, 5), "Testing start index from mid point")
|
||||
assert.Equal(t, int64(9), getPagingStartIndex(-1, 10, 5), "Testing start index with negative offset")
|
||||
assert.Equal(t, int64(0), getPagingStartIndex(15, 10, 5), "Testing start index with large offset")
|
||||
assert.Equal(t, int64(9), getPagingStartIndex(0, 10, 0), "Testing start index with zero count")
|
||||
}
|
||||
25
service/internal/executor/executor_unix.go
Normal file
25
service/internal/executor/executor_unix.go
Normal file
@@ -0,0 +1,25 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package executor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func (e *Executor) Kill(execReq *InternalLogEntry) error {
|
||||
// A negative PID means to kill the whole process group. This is *nix specific behavior.
|
||||
return syscall.Kill(-execReq.Process.Pid, syscall.SIGKILL)
|
||||
}
|
||||
|
||||
func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cmd {
|
||||
cmd := exec.CommandContext(ctx, "sh", "-c", finalParsedCommand)
|
||||
|
||||
// This is to ensure that the process group is killed when the parent process is killed.
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
|
||||
return cmd
|
||||
|
||||
}
|
||||
17
service/internal/executor/executor_windows.go
Normal file
17
service/internal/executor/executor_windows.go
Normal file
@@ -0,0 +1,17 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package executor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func (e *Executor) Kill(execReq *InternalLogEntry) error {
|
||||
return execReq.Process.Kill()
|
||||
}
|
||||
|
||||
func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cmd {
|
||||
return exec.CommandContext(ctx, "cmd", "/C", finalParsedCommand)
|
||||
}
|
||||
@@ -148,7 +148,7 @@ func processDebounce(ctx *watchContext) {
|
||||
debounceWriteLog[ctx.filename] = logEntry
|
||||
}
|
||||
|
||||
log.Infof("fsnotify event %+v", logEntry)
|
||||
log.Debugf("fsnotify event %+v", logEntry)
|
||||
|
||||
if logEntry.callbackComplete || logEntry.callbackWrapper == nil {
|
||||
log.Debugf("fsnotify event callback queued within debounce delay: %v", ctx.filename)
|
||||
38
service/internal/filehelper/file_write.go
Normal file
38
service/internal/filehelper/file_write.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package filehelper
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var writeFileMutex sync.Mutex
|
||||
|
||||
func WriteFile(filename string, out []byte) {
|
||||
writeFileMutex.Lock()
|
||||
|
||||
defer writeFileMutex.Unlock()
|
||||
|
||||
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
||||
handle, err := os.Create(filename)
|
||||
handle.Close()
|
||||
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
}).Errorf("Failed to create %v", filename)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
err := os.WriteFile(filename, out, 0600)
|
||||
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
}).Errorf("Failed to write session to %v", filename)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,18 @@ package grpcapi
|
||||
|
||||
import (
|
||||
ctx "context"
|
||||
pb "github.com/OliveTin/OliveTin/gen/grpc"
|
||||
|
||||
apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/genproto/googleapis/api/httpbody"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"errors"
|
||||
"net"
|
||||
"sort"
|
||||
|
||||
acl "github.com/OliveTin/OliveTin/internal/acl"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
@@ -25,34 +28,40 @@ var (
|
||||
|
||||
type oliveTinAPI struct {
|
||||
// Uncomment this if you want to allow undefined methods during dev.
|
||||
// pb.UnimplementedOliveTinApiServiceServer
|
||||
// apiv1.UnimplementedOliveTinApiServiceServer
|
||||
|
||||
executor *executor.Executor
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) KillAction(ctx ctx.Context, req *pb.KillActionRequest) (*pb.KillActionResponse, error) {
|
||||
ret := &pb.KillActionResponse{
|
||||
func (api *oliveTinAPI) KillAction(ctx ctx.Context, req *apiv1.KillActionRequest) (*apiv1.KillActionResponse, error) {
|
||||
ret := &apiv1.KillActionResponse{
|
||||
ExecutionTrackingId: req.ExecutionTrackingId,
|
||||
}
|
||||
|
||||
execReq, found := api.executor.Logs[req.ExecutionTrackingId]
|
||||
execReqLogEntry, found := api.executor.GetLog(req.ExecutionTrackingId)
|
||||
|
||||
ret.Found = found
|
||||
|
||||
if found {
|
||||
err := execReq.Process.Kill()
|
||||
log.Warnf("Killing execution request by tracking ID: %v", req.ExecutionTrackingId)
|
||||
|
||||
if err == nil {
|
||||
err := api.executor.Kill(execReqLogEntry)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Killing execution request err: %v", err)
|
||||
ret.AlreadyCompleted = true
|
||||
ret.Killed = false
|
||||
} else {
|
||||
ret.Killed = true
|
||||
}
|
||||
} else {
|
||||
log.Warnf("Killing execution request not possible - not found by tracking ID: %v", req.ExecutionTrackingId)
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *pb.StartActionRequest) (*pb.StartActionResponse, error) {
|
||||
func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *apiv1.StartActionRequest) (*apiv1.StartActionResponse, error) {
|
||||
args := make(map[string]string)
|
||||
|
||||
for _, arg := range req.Arguments {
|
||||
@@ -63,25 +72,70 @@ func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *pb.StartActionRequest)
|
||||
pair := api.executor.MapActionIdToBinding[req.ActionId]
|
||||
api.executor.MapActionIdToBindingLock.RUnlock()
|
||||
|
||||
if pair == nil || pair.Action == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "Action not found.")
|
||||
}
|
||||
|
||||
authenticatedUser := acl.UserFromContext(ctx, cfg)
|
||||
|
||||
execReq := executor.ExecutionRequest{
|
||||
Action: pair.Action,
|
||||
EntityPrefix: pair.EntityPrefix,
|
||||
TrackingID: req.UniqueTrackingId,
|
||||
Arguments: args,
|
||||
AuthenticatedUser: acl.UserFromContext(ctx, cfg),
|
||||
AuthenticatedUser: authenticatedUser,
|
||||
Cfg: cfg,
|
||||
}
|
||||
|
||||
api.executor.ExecRequest(&execReq)
|
||||
|
||||
return &pb.StartActionResponse{
|
||||
return &apiv1.StartActionResponse{
|
||||
ExecutionTrackingId: execReq.TrackingID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *pb.StartActionAndWaitRequest) (*pb.StartActionAndWaitResponse, error) {
|
||||
func (api *oliveTinAPI) PasswordHash(ctx ctx.Context, req *apiv1.PasswordHashRequest) (*httpbody.HttpBody, error) {
|
||||
hash, err := createHash(req.Password)
|
||||
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "Error creating hash.")
|
||||
}
|
||||
|
||||
ret := &httpbody.HttpBody{
|
||||
ContentType: "text/plain",
|
||||
Data: []byte("Your password hash is: " + hash),
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) LocalUserLogin(ctx ctx.Context, req *apiv1.LocalUserLoginRequest) (*apiv1.LocalUserLoginResponse, error) {
|
||||
match := checkUserPassword(cfg, req.Username, req.Password)
|
||||
|
||||
if match {
|
||||
grpc.SendHeader(ctx, metadata.Pairs("set-username", req.Username))
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"username": req.Username,
|
||||
}).Info("LocalUserLogin: User logged in successfully.")
|
||||
} else {
|
||||
log.WithFields(log.Fields{
|
||||
"username": req.Username,
|
||||
}).Warn("LocalUserLogin: User login failed.")
|
||||
}
|
||||
|
||||
return &apiv1.LocalUserLoginResponse{
|
||||
Success: match,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *apiv1.StartActionAndWaitRequest) (*apiv1.StartActionAndWaitResponse, error) {
|
||||
args := make(map[string]string)
|
||||
|
||||
for _, arg := range req.Arguments {
|
||||
args[arg.Name] = arg.Value
|
||||
}
|
||||
|
||||
execReq := executor.ExecutionRequest{
|
||||
Action: api.executor.FindActionBindingByID(req.ActionId),
|
||||
TrackingID: uuid.NewString(),
|
||||
@@ -93,10 +147,10 @@ func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *pb.StartActionA
|
||||
wg, _ := api.executor.ExecRequest(&execReq)
|
||||
wg.Wait()
|
||||
|
||||
internalLogEntry, ok := api.executor.Logs[execReq.TrackingID]
|
||||
internalLogEntry, ok := api.executor.GetLog(execReq.TrackingID)
|
||||
|
||||
if ok {
|
||||
return &pb.StartActionAndWaitResponse{
|
||||
return &apiv1.StartActionAndWaitResponse{
|
||||
LogEntry: internalLogEntryToPb(internalLogEntry),
|
||||
}, nil
|
||||
} else {
|
||||
@@ -104,7 +158,7 @@ func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *pb.StartActionA
|
||||
}
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) StartActionByGet(ctx ctx.Context, req *pb.StartActionByGetRequest) (*pb.StartActionByGetResponse, error) {
|
||||
func (api *oliveTinAPI) StartActionByGet(ctx ctx.Context, req *apiv1.StartActionByGetRequest) (*apiv1.StartActionByGetResponse, error) {
|
||||
args := make(map[string]string)
|
||||
|
||||
execReq := executor.ExecutionRequest{
|
||||
@@ -117,12 +171,12 @@ func (api *oliveTinAPI) StartActionByGet(ctx ctx.Context, req *pb.StartActionByG
|
||||
|
||||
_, uniqueTrackingId := api.executor.ExecRequest(&execReq)
|
||||
|
||||
return &pb.StartActionByGetResponse{
|
||||
return &apiv1.StartActionByGetResponse{
|
||||
ExecutionTrackingId: uniqueTrackingId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *pb.StartActionByGetAndWaitRequest) (*pb.StartActionByGetAndWaitResponse, error) {
|
||||
func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *apiv1.StartActionByGetAndWaitRequest) (*apiv1.StartActionByGetAndWaitResponse, error) {
|
||||
args := make(map[string]string)
|
||||
|
||||
execReq := executor.ExecutionRequest{
|
||||
@@ -136,24 +190,25 @@ func (api *oliveTinAPI) StartActionByGetAndWait(ctx ctx.Context, req *pb.StartAc
|
||||
wg, _ := api.executor.ExecRequest(&execReq)
|
||||
wg.Wait()
|
||||
|
||||
internalLogEntry, ok := api.executor.Logs[execReq.TrackingID]
|
||||
internalLogEntry, ok := api.executor.GetLog(execReq.TrackingID)
|
||||
|
||||
if ok {
|
||||
return &pb.StartActionByGetAndWaitResponse{
|
||||
return &apiv1.StartActionByGetAndWaitResponse{
|
||||
LogEntry: internalLogEntryToPb(internalLogEntry),
|
||||
}, nil
|
||||
} else {
|
||||
return nil, errors.New("Execution not found!")
|
||||
return nil, status.Errorf(codes.NotFound, "Execution not found.")
|
||||
}
|
||||
}
|
||||
|
||||
func internalLogEntryToPb(logEntry *executor.InternalLogEntry) *pb.LogEntry {
|
||||
return &pb.LogEntry{
|
||||
func internalLogEntryToPb(logEntry *executor.InternalLogEntry) *apiv1.LogEntry {
|
||||
return &apiv1.LogEntry{
|
||||
ActionTitle: logEntry.ActionTitle,
|
||||
ActionIcon: logEntry.ActionIcon,
|
||||
ActionId: logEntry.ActionId,
|
||||
DatetimeStarted: logEntry.DatetimeStarted.Format("2006-01-02 15:04:05"),
|
||||
DatetimeFinished: logEntry.DatetimeFinished.Format("2006-01-02 15:04:05"),
|
||||
DatetimeIndex: logEntry.Index,
|
||||
Output: logEntry.Output,
|
||||
TimedOut: logEntry.TimedOut,
|
||||
Blocked: logEntry.Blocked,
|
||||
@@ -162,11 +217,12 @@ func internalLogEntryToPb(logEntry *executor.InternalLogEntry) *pb.LogEntry {
|
||||
ExecutionTrackingId: logEntry.ExecutionTrackingID,
|
||||
ExecutionStarted: logEntry.ExecutionStarted,
|
||||
ExecutionFinished: logEntry.ExecutionFinished,
|
||||
User: logEntry.Username,
|
||||
}
|
||||
}
|
||||
|
||||
func getExecutionStatusByTrackingID(api *oliveTinAPI, executionTrackingId string) *executor.InternalLogEntry {
|
||||
logEntry, ok := api.executor.Logs[executionTrackingId]
|
||||
logEntry, ok := api.executor.GetLog(executionTrackingId)
|
||||
|
||||
if !ok {
|
||||
return nil
|
||||
@@ -178,33 +234,41 @@ func getExecutionStatusByTrackingID(api *oliveTinAPI, executionTrackingId string
|
||||
func getMostRecentExecutionStatusById(api *oliveTinAPI, actionId string) *executor.InternalLogEntry {
|
||||
var ile *executor.InternalLogEntry
|
||||
|
||||
for _, candidateLe := range api.executor.Logs {
|
||||
if actionId == candidateLe.ActionId {
|
||||
ile = candidateLe
|
||||
}
|
||||
logs := api.executor.GetLogsByActionId(actionId)
|
||||
|
||||
if len(logs) == 0 {
|
||||
return nil
|
||||
} else {
|
||||
// Get last log entry
|
||||
ile = logs[len(logs)-1]
|
||||
}
|
||||
|
||||
return ile
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *pb.ExecutionStatusRequest) (*pb.ExecutionStatusResponse, error) {
|
||||
res := &pb.ExecutionStatusResponse{}
|
||||
func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *apiv1.ExecutionStatusRequest) (*apiv1.ExecutionStatusResponse, error) {
|
||||
res := &apiv1.ExecutionStatusResponse{}
|
||||
|
||||
var ile *executor.InternalLogEntry
|
||||
|
||||
if req.ExecutionTrackingId != "" {
|
||||
ile = getExecutionStatusByTrackingID(api, req.ExecutionTrackingId)
|
||||
|
||||
} else {
|
||||
ile = getMostRecentExecutionStatusById(api, req.ActionId)
|
||||
}
|
||||
|
||||
res.LogEntry = internalLogEntryToPb(ile)
|
||||
if ile == nil {
|
||||
return nil, status.Error(codes.NotFound, "Execution not found")
|
||||
} else {
|
||||
res.LogEntry = internalLogEntryToPb(ile)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
/**
|
||||
func (api *oliveTinAPI) WatchExecution(req *pb.WatchExecutionRequest, srv pb.OliveTinApi_WatchExecutionServer) error {
|
||||
func (api *oliveTinAPI) WatchExecution(req *apiv1.WatchExecutionRequest, srv apiv1.OliveTinApi_WatchExecutionServer) error {
|
||||
log.Infof("Watch")
|
||||
|
||||
if logEntry, ok := api.executor.Logs[req.ExecutionUuid]; !ok {
|
||||
@@ -220,7 +284,7 @@ func (api *oliveTinAPI) WatchExecution(req *pb.WatchExecutionRequest, srv pb.Oli
|
||||
|
||||
log.Infof("%v %v", red, err)
|
||||
|
||||
srv.Send(&pb.WatchExecutionUpdate{
|
||||
srv.Send(&apiv1.WatchExecutionUpdate{
|
||||
Update: string(tmp),
|
||||
})
|
||||
}
|
||||
@@ -231,47 +295,60 @@ func (api *oliveTinAPI) WatchExecution(req *pb.WatchExecutionRequest, srv pb.Oli
|
||||
}
|
||||
*/
|
||||
|
||||
func (api *oliveTinAPI) GetDashboardComponents(ctx ctx.Context, req *pb.GetDashboardComponentsRequest) (*pb.GetDashboardComponentsResponse, error) {
|
||||
func (api *oliveTinAPI) Logout(ctx ctx.Context, req *apiv1.LogoutRequest) (*httpbody.HttpBody, error) {
|
||||
user := acl.UserFromContext(ctx, cfg)
|
||||
|
||||
grpc.SendHeader(ctx, metadata.Pairs("logout-provider", user.Provider))
|
||||
grpc.SendHeader(ctx, metadata.Pairs("logout-sid", user.SID))
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) GetDashboardComponents(ctx ctx.Context, req *apiv1.GetDashboardComponentsRequest) (*apiv1.GetDashboardComponentsResponse, error) {
|
||||
user := acl.UserFromContext(ctx, cfg)
|
||||
|
||||
if user.IsGuest() && cfg.AuthRequireGuestsToLogin {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "Guests are not allowed to access the dashboard.")
|
||||
}
|
||||
|
||||
res := buildDashboardResponse(api.executor, cfg, user)
|
||||
|
||||
if len(res.Actions) == 0 {
|
||||
log.Warn("Zero actions found - check that you have some actions defined, with a view permission")
|
||||
log.WithFields(log.Fields{
|
||||
"username": user.Username,
|
||||
"usergroup": user.Usergroup,
|
||||
"provider": user.Provider,
|
||||
"acls": user.Acls,
|
||||
"availableActions": len(cfg.Actions),
|
||||
}).Warn("Zero actions found for user")
|
||||
}
|
||||
|
||||
log.Tracef("GetDashboardComponents: %v", res)
|
||||
|
||||
dashboardCfgToPb(res, cfg.Dashboards, cfg)
|
||||
|
||||
res.AuthenticatedUser = user.Username
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *pb.GetLogsRequest) (*pb.GetLogsResponse, error) {
|
||||
func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *apiv1.GetLogsRequest) (*apiv1.GetLogsResponse, error) {
|
||||
user := acl.UserFromContext(ctx, cfg)
|
||||
|
||||
ret := &pb.GetLogsResponse{}
|
||||
ret := &apiv1.GetLogsResponse{}
|
||||
|
||||
// TODO Limit to 10 entries or something to prevent browser lag.
|
||||
logEntries, countRemaining := api.executor.GetLogTrackingIds(req.StartOffset, cfg.LogHistoryPageSize)
|
||||
|
||||
for trackingId, logEntry := range api.executor.Logs {
|
||||
for _, logEntry := range logEntries {
|
||||
action := cfg.FindAction(logEntry.ActionTitle)
|
||||
|
||||
if action == nil || acl.IsAllowedLogs(cfg, user, action) {
|
||||
pbLogEntry := internalLogEntryToPb(logEntry)
|
||||
pbLogEntry.ExecutionTrackingId = trackingId
|
||||
|
||||
ret.Logs = append(ret.Logs, pbLogEntry)
|
||||
}
|
||||
}
|
||||
|
||||
sorter := func(i, j int) bool {
|
||||
return ret.Logs[i].DatetimeStarted < ret.Logs[j].DatetimeStarted
|
||||
}
|
||||
|
||||
sort.Slice(ret.Logs, sorter)
|
||||
ret.CountRemaining = countRemaining
|
||||
ret.PageSize = cfg.LogHistoryPageSize
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
@@ -281,7 +358,7 @@ This function is ONLY a helper for the UI - the arguments are validated properly
|
||||
on the StartAction -> Executor chain. This is here basically to provide helpful
|
||||
error messages more quickly before starting the action.
|
||||
*/
|
||||
func (api *oliveTinAPI) ValidateArgumentType(ctx ctx.Context, req *pb.ValidateArgumentTypeRequest) (*pb.ValidateArgumentTypeResponse, error) {
|
||||
func (api *oliveTinAPI) ValidateArgumentType(ctx ctx.Context, req *apiv1.ValidateArgumentTypeRequest) (*apiv1.ValidateArgumentTypeResponse, error) {
|
||||
err := executor.TypeSafetyCheck("", req.Value, req.Type)
|
||||
desc := ""
|
||||
|
||||
@@ -289,17 +366,21 @@ func (api *oliveTinAPI) ValidateArgumentType(ctx ctx.Context, req *pb.ValidateAr
|
||||
desc = err.Error()
|
||||
}
|
||||
|
||||
return &pb.ValidateArgumentTypeResponse{
|
||||
return &apiv1.ValidateArgumentTypeResponse{
|
||||
Valid: err == nil,
|
||||
Description: desc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) WhoAmI(ctx ctx.Context, req *pb.WhoAmIRequest) (*pb.WhoAmIResponse, error) {
|
||||
func (api *oliveTinAPI) WhoAmI(ctx ctx.Context, req *apiv1.WhoAmIRequest) (*apiv1.WhoAmIResponse, error) {
|
||||
user := acl.UserFromContext(ctx, cfg)
|
||||
|
||||
res := &pb.WhoAmIResponse{
|
||||
res := &apiv1.WhoAmIResponse{
|
||||
AuthenticatedUser: user.Username,
|
||||
Usergroup: user.Usergroup,
|
||||
Provider: user.Provider,
|
||||
Sid: user.SID,
|
||||
Acls: user.Acls,
|
||||
}
|
||||
|
||||
log.Warnf("usergroup: %v", user.Usergroup)
|
||||
@@ -307,7 +388,7 @@ func (api *oliveTinAPI) WhoAmI(ctx ctx.Context, req *pb.WhoAmIRequest) (*pb.WhoA
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) SosReport(ctx ctx.Context, req *pb.SosReportRequest) (*httpbody.HttpBody, error) {
|
||||
func (api *oliveTinAPI) SosReport(ctx ctx.Context, req *apiv1.SosReportRequest) (*httpbody.HttpBody, error) {
|
||||
sos := installationinfo.GetSosReport()
|
||||
|
||||
if !cfg.InsecureAllowDumpSos {
|
||||
@@ -323,8 +404,8 @@ func (api *oliveTinAPI) SosReport(ctx ctx.Context, req *pb.SosReportRequest) (*h
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) DumpVars(ctx ctx.Context, req *pb.DumpVarsRequest) (*pb.DumpVarsResponse, error) {
|
||||
res := &pb.DumpVarsResponse{}
|
||||
func (api *oliveTinAPI) DumpVars(ctx ctx.Context, req *apiv1.DumpVarsRequest) (*apiv1.DumpVarsResponse, error) {
|
||||
res := &apiv1.DumpVarsResponse{}
|
||||
|
||||
if !cfg.InsecureAllowDumpVars {
|
||||
res.Alert = "Dumping variables is not allowed by default because it is insecure."
|
||||
@@ -338,9 +419,9 @@ func (api *oliveTinAPI) DumpVars(ctx ctx.Context, req *pb.DumpVarsRequest) (*pb.
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) DumpPublicIdActionMap(ctx ctx.Context, req *pb.DumpPublicIdActionMapRequest) (*pb.DumpPublicIdActionMapResponse, error) {
|
||||
res := &pb.DumpPublicIdActionMapResponse{}
|
||||
res.Contents = make(map[string]*pb.ActionEntityPair)
|
||||
func (api *oliveTinAPI) DumpPublicIdActionMap(ctx ctx.Context, req *apiv1.DumpPublicIdActionMapRequest) (*apiv1.DumpPublicIdActionMapResponse, error) {
|
||||
res := &apiv1.DumpPublicIdActionMapResponse{}
|
||||
res.Contents = make(map[string]*apiv1.ActionEntityPair)
|
||||
|
||||
if !cfg.InsecureAllowDumpActionMap {
|
||||
res.Alert = "Dumping Public IDs is disallowed."
|
||||
@@ -351,7 +432,7 @@ func (api *oliveTinAPI) DumpPublicIdActionMap(ctx ctx.Context, req *pb.DumpPubli
|
||||
api.executor.MapActionIdToBindingLock.RLock()
|
||||
|
||||
for k, v := range api.executor.MapActionIdToBinding {
|
||||
res.Contents[k] = &pb.ActionEntityPair{
|
||||
res.Contents[k] = &apiv1.ActionEntityPair{
|
||||
ActionTitle: v.Action.Title,
|
||||
EntityPrefix: v.EntityPrefix,
|
||||
}
|
||||
@@ -364,8 +445,8 @@ func (api *oliveTinAPI) DumpPublicIdActionMap(ctx ctx.Context, req *pb.DumpPubli
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) GetReadyz(ctx ctx.Context, req *pb.GetReadyzRequest) (*pb.GetReadyzResponse, error) {
|
||||
res := &pb.GetReadyzResponse{
|
||||
func (api *oliveTinAPI) GetReadyz(ctx ctx.Context, req *apiv1.GetReadyzRequest) (*apiv1.GetReadyzResponse, error) {
|
||||
res := &apiv1.GetReadyzResponse{
|
||||
Status: "OK",
|
||||
}
|
||||
|
||||
@@ -376,6 +457,10 @@ func (api *oliveTinAPI) GetReadyz(ctx ctx.Context, req *pb.GetReadyzRequest) (*p
|
||||
func Start(globalConfig *config.Config, ex *executor.Executor) {
|
||||
cfg = globalConfig
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"address": cfg.ListenAddressGrpcActions,
|
||||
}).Info("Starting gRPC API")
|
||||
|
||||
lis, err := net.Listen("tcp", cfg.ListenAddressGrpcActions)
|
||||
|
||||
if err != nil {
|
||||
@@ -383,7 +468,7 @@ func Start(globalConfig *config.Config, ex *executor.Executor) {
|
||||
}
|
||||
|
||||
grpcServer := grpc.NewServer()
|
||||
pb.RegisterOliveTinApiServiceServer(grpcServer, newServer(ex))
|
||||
apiv1.RegisterOliveTinApiServiceServer(grpcServer, newServer(ex))
|
||||
|
||||
err = grpcServer.Serve(lis)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package grpcapi
|
||||
|
||||
import (
|
||||
pb "github.com/OliveTin/OliveTin/gen/grpc"
|
||||
apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
|
||||
acl "github.com/OliveTin/OliveTin/internal/acl"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
executor "github.com/OliveTin/OliveTin/internal/executor"
|
||||
@@ -9,8 +9,11 @@ import (
|
||||
"sort"
|
||||
)
|
||||
|
||||
func buildDashboardResponse(ex *executor.Executor, cfg *config.Config, user *acl.AuthenticatedUser) *pb.GetDashboardComponentsResponse {
|
||||
res := &pb.GetDashboardComponentsResponse{}
|
||||
func buildDashboardResponse(ex *executor.Executor, cfg *config.Config, user *acl.AuthenticatedUser) *apiv1.GetDashboardComponentsResponse {
|
||||
res := &apiv1.GetDashboardComponentsResponse{
|
||||
AuthenticatedUser: user.Username,
|
||||
AuthenticatedUserProvider: user.Provider,
|
||||
}
|
||||
|
||||
ex.MapActionIdToBindingLock.RLock()
|
||||
|
||||
@@ -35,10 +38,10 @@ func buildDashboardResponse(ex *executor.Executor, cfg *config.Config, user *acl
|
||||
return res
|
||||
}
|
||||
|
||||
func buildAction(actionId string, actionBinding *executor.ActionBinding, user *acl.AuthenticatedUser) *pb.Action {
|
||||
func buildAction(actionId string, actionBinding *executor.ActionBinding, user *acl.AuthenticatedUser) *apiv1.Action {
|
||||
action := actionBinding.Action
|
||||
|
||||
btn := pb.Action{
|
||||
btn := apiv1.Action{
|
||||
Id: actionId,
|
||||
Title: sv.ReplaceEntityVars(actionBinding.EntityPrefix, action.Title),
|
||||
Icon: action.Icon,
|
||||
@@ -48,7 +51,7 @@ func buildAction(actionId string, actionBinding *executor.ActionBinding, user *a
|
||||
}
|
||||
|
||||
for _, cfgArg := range action.Arguments {
|
||||
pbArg := pb.ActionArgument{
|
||||
pbArg := apiv1.ActionArgument{
|
||||
Name: cfgArg.Name,
|
||||
Title: cfgArg.Title,
|
||||
Type: cfgArg.Type,
|
||||
@@ -64,7 +67,7 @@ func buildAction(actionId string, actionBinding *executor.ActionBinding, user *a
|
||||
return &btn
|
||||
}
|
||||
|
||||
func buildChoices(arg config.ActionArgument) []*pb.ActionArgumentChoice {
|
||||
func buildChoices(arg config.ActionArgument) []*apiv1.ActionArgumentChoice {
|
||||
if arg.Entity != "" && len(arg.Choices) == 1 {
|
||||
return buildChoicesEntity(arg.Choices[0], arg.Entity)
|
||||
} else {
|
||||
@@ -72,15 +75,15 @@ func buildChoices(arg config.ActionArgument) []*pb.ActionArgumentChoice {
|
||||
}
|
||||
}
|
||||
|
||||
func buildChoicesEntity(firstChoice config.ActionArgumentChoice, entityTitle string) []*pb.ActionArgumentChoice {
|
||||
ret := []*pb.ActionArgumentChoice{}
|
||||
func buildChoicesEntity(firstChoice config.ActionArgumentChoice, entityTitle string) []*apiv1.ActionArgumentChoice {
|
||||
ret := []*apiv1.ActionArgumentChoice{}
|
||||
|
||||
entityCount := sv.GetEntityCount(entityTitle)
|
||||
|
||||
for i := 0; i < entityCount; i++ {
|
||||
prefix := sv.GetEntityPrefix(entityTitle, i)
|
||||
|
||||
ret = append(ret, &pb.ActionArgumentChoice{
|
||||
ret = append(ret, &apiv1.ActionArgumentChoice{
|
||||
Value: sv.ReplaceEntityVars(prefix, firstChoice.Value),
|
||||
Title: sv.ReplaceEntityVars(prefix, firstChoice.Title),
|
||||
})
|
||||
@@ -89,11 +92,11 @@ func buildChoicesEntity(firstChoice config.ActionArgumentChoice, entityTitle str
|
||||
return ret
|
||||
}
|
||||
|
||||
func buildChoicesSimple(choices []config.ActionArgumentChoice) []*pb.ActionArgumentChoice {
|
||||
ret := []*pb.ActionArgumentChoice{}
|
||||
func buildChoicesSimple(choices []config.ActionArgumentChoice) []*apiv1.ActionArgumentChoice {
|
||||
ret := []*apiv1.ActionArgumentChoice{}
|
||||
|
||||
for _, cfgChoice := range choices {
|
||||
pbChoice := pb.ActionArgumentChoice{
|
||||
pbChoice := apiv1.ActionArgumentChoice{
|
||||
Value: cfgChoice.Value,
|
||||
Title: cfgChoice.Title,
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
package grpcapi
|
||||
|
||||
import (
|
||||
pb "github.com/OliveTin/OliveTin/gen/grpc"
|
||||
apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func dashboardCfgToPb(res *pb.GetDashboardComponentsResponse, dashboards []*config.DashboardComponent, cfg *config.Config) {
|
||||
func dashboardCfgToPb(res *apiv1.GetDashboardComponentsResponse, dashboards []*config.DashboardComponent, cfg *config.Config) {
|
||||
for _, dashboard := range dashboards {
|
||||
res.Dashboards = append(res.Dashboards, &pb.DashboardComponent{
|
||||
res.Dashboards = append(res.Dashboards, &apiv1.DashboardComponent{
|
||||
Type: "dashboard",
|
||||
Title: dashboard.Title,
|
||||
Contents: getDashboardComponentContents(dashboard, cfg),
|
||||
@@ -16,8 +16,8 @@ func dashboardCfgToPb(res *pb.GetDashboardComponentsResponse, dashboards []*conf
|
||||
}
|
||||
}
|
||||
|
||||
func getDashboardComponentContents(dashboard *config.DashboardComponent, cfg *config.Config) []*pb.DashboardComponent {
|
||||
ret := make([]*pb.DashboardComponent, 0)
|
||||
func getDashboardComponentContents(dashboard *config.DashboardComponent, cfg *config.Config) []*apiv1.DashboardComponent {
|
||||
ret := make([]*apiv1.DashboardComponent, 0)
|
||||
|
||||
for _, subitem := range dashboard.Contents {
|
||||
if subitem.Type == "fieldset" && subitem.Entity != "" {
|
||||
@@ -25,11 +25,12 @@ func getDashboardComponentContents(dashboard *config.DashboardComponent, cfg *co
|
||||
continue
|
||||
}
|
||||
|
||||
newitem := &pb.DashboardComponent{
|
||||
newitem := &apiv1.DashboardComponent{
|
||||
Title: subitem.Title,
|
||||
Type: getDashboardComponentType(&subitem),
|
||||
Contents: getDashboardComponentContents(&subitem, cfg),
|
||||
Icon: getDashboardComponentIcon(&subitem, cfg),
|
||||
CssClass: subitem.CssClass,
|
||||
}
|
||||
|
||||
ret = append(ret, newitem)
|
||||
@@ -1,13 +1,13 @@
|
||||
package grpcapi
|
||||
|
||||
import (
|
||||
pb "github.com/OliveTin/OliveTin/gen/grpc"
|
||||
apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
sv "github.com/OliveTin/OliveTin/internal/stringvariables"
|
||||
)
|
||||
|
||||
func buildEntityFieldsets(entityTitle string, tpl *config.DashboardComponent) []*pb.DashboardComponent {
|
||||
ret := make([]*pb.DashboardComponent, 0)
|
||||
func buildEntityFieldsets(entityTitle string, tpl *config.DashboardComponent) []*apiv1.DashboardComponent {
|
||||
ret := make([]*apiv1.DashboardComponent, 0)
|
||||
|
||||
entityCount := sv.GetEntityCount(entityTitle)
|
||||
|
||||
@@ -18,21 +18,23 @@ func buildEntityFieldsets(entityTitle string, tpl *config.DashboardComponent) []
|
||||
return ret
|
||||
}
|
||||
|
||||
func buildEntityFieldset(tpl *config.DashboardComponent, entityTitle string, entityIndex int) *pb.DashboardComponent {
|
||||
func buildEntityFieldset(tpl *config.DashboardComponent, entityTitle string, entityIndex int) *apiv1.DashboardComponent {
|
||||
prefix := sv.GetEntityPrefix(entityTitle, entityIndex)
|
||||
|
||||
return &pb.DashboardComponent{
|
||||
return &apiv1.DashboardComponent{
|
||||
Title: sv.ReplaceEntityVars(prefix, tpl.Title),
|
||||
Type: "fieldset",
|
||||
Contents: buildEntityFieldsetContents(tpl.Contents, prefix),
|
||||
CssClass: sv.ReplaceEntityVars(prefix, tpl.CssClass),
|
||||
}
|
||||
}
|
||||
|
||||
func buildEntityFieldsetContents(contents []config.DashboardComponent, prefix string) []*pb.DashboardComponent {
|
||||
ret := make([]*pb.DashboardComponent, 0)
|
||||
func buildEntityFieldsetContents(contents []config.DashboardComponent, prefix string) []*apiv1.DashboardComponent {
|
||||
ret := make([]*apiv1.DashboardComponent, 0)
|
||||
|
||||
for _, subitem := range contents {
|
||||
clone := &pb.DashboardComponent{}
|
||||
clone := &apiv1.DashboardComponent{}
|
||||
clone.CssClass = sv.ReplaceEntityVars(prefix, subitem.CssClass)
|
||||
|
||||
if subitem.Type == "" || subitem.Type == "link" {
|
||||
clone.Type = "link"
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
pb "github.com/OliveTin/OliveTin/gen/grpc"
|
||||
apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
"github.com/OliveTin/OliveTin/internal/executor"
|
||||
)
|
||||
@@ -26,7 +26,7 @@ func initServer(cfg *config.Config) *executor.Executor {
|
||||
|
||||
lis = bufconn.Listen(bufSize)
|
||||
s := grpc.NewServer()
|
||||
pb.RegisterOliveTinApiServiceServer(s, newServer(ex))
|
||||
apiv1.RegisterOliveTinApiServiceServer(s, newServer(ex))
|
||||
|
||||
go func() {
|
||||
if err := s.Serve(lis); err != nil {
|
||||
@@ -41,7 +41,7 @@ func bufDialer(context.Context, string) (net.Conn, error) {
|
||||
return lis.Dial()
|
||||
}
|
||||
|
||||
func getNewTestServerAndClient(t *testing.T, injectedConfig *config.Config) (*grpc.ClientConn, pb.OliveTinApiServiceClient) {
|
||||
func getNewTestServerAndClient(t *testing.T, injectedConfig *config.Config) (*grpc.ClientConn, apiv1.OliveTinApiServiceClient) {
|
||||
cfg = injectedConfig
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -52,7 +52,7 @@ func getNewTestServerAndClient(t *testing.T, injectedConfig *config.Config) (*gr
|
||||
t.Fatalf("Failed to dial bufnet: %v", err)
|
||||
}
|
||||
|
||||
client := pb.NewOliveTinApiServiceClient(conn)
|
||||
client := apiv1.NewOliveTinApiServiceClient(conn)
|
||||
|
||||
return conn, client
|
||||
}
|
||||
@@ -72,7 +72,7 @@ func TestGetActionsAndStart(t *testing.T) {
|
||||
|
||||
conn, client := getNewTestServerAndClient(t, cfg)
|
||||
|
||||
respGb, err := client.GetDashboardComponents(context.Background(), &pb.GetDashboardComponentsRequest{})
|
||||
respGb, err := client.GetDashboardComponents(context.Background(), &apiv1.GetDashboardComponentsRequest{})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("GetDashboardComponentsRequest: %v", err)
|
||||
@@ -84,7 +84,7 @@ func TestGetActionsAndStart(t *testing.T) {
|
||||
|
||||
log.Printf("Response: %+v", respGb)
|
||||
|
||||
respSa, err := client.StartAction(context.Background(), &pb.StartActionRequest{ActionId: "blat"})
|
||||
respSa, err := client.StartAction(context.Background(), &apiv1.StartActionRequest{ActionId: "blat"})
|
||||
|
||||
assert.Nil(t, err, "Empty err after start action")
|
||||
assert.NotNil(t, respSa, "Empty err after start action")
|
||||
62
service/internal/grpcapi/local_user_login.go
Normal file
62
service/internal/grpcapi/local_user_login.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package grpcapi
|
||||
|
||||
import (
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
"github.com/alexedwards/argon2id"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var defaultParams = argon2id.Params{
|
||||
Memory: 64 * 1024,
|
||||
Iterations: 4,
|
||||
Parallelism: uint8(runtime.NumCPU()),
|
||||
SaltLength: 16,
|
||||
KeyLength: 32,
|
||||
}
|
||||
|
||||
func createHash(password string) (string, error) {
|
||||
hash, err := argon2id.CreateHash(password, &defaultParams)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Error creating hash: ", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
func comparePasswordAndHash(password, hash string) bool {
|
||||
match, err := argon2id.ComparePasswordAndHash(password, hash)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Error comparing password and hash: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return match
|
||||
}
|
||||
|
||||
func checkUserPassword(cfg *config.Config, username, password string) bool {
|
||||
for _, user := range cfg.AuthLocalUsers.Users {
|
||||
if user.Username == username {
|
||||
match := comparePasswordAndHash(password, user.Password)
|
||||
|
||||
if match {
|
||||
return true
|
||||
} else {
|
||||
log.WithFields(log.Fields{
|
||||
"username": username,
|
||||
}).Warn("Password does not match for user")
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"username": username,
|
||||
}).Warn("Failed to check password for user, as username was not found")
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -7,9 +7,10 @@ import (
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
"net/http"
|
||||
|
||||
gw "github.com/OliveTin/OliveTin/gen/grpc"
|
||||
apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
|
||||
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
cors "github.com/OliveTin/OliveTin/internal/cors"
|
||||
@@ -45,28 +46,104 @@ func parseHttpHeaderForAuth(req *http.Request) (string, string) {
|
||||
return username[0], ""
|
||||
}
|
||||
|
||||
//gocyclo:ignore
|
||||
func parseRequestMetadata(ctx context.Context, req *http.Request) metadata.MD {
|
||||
username := ""
|
||||
usergroup := ""
|
||||
provider := "unknown"
|
||||
sid := ""
|
||||
|
||||
if cfg.AuthJwtCookieName != "" {
|
||||
username, usergroup = parseJwtCookie(req)
|
||||
provider = "jwt-cookie"
|
||||
}
|
||||
|
||||
if cfg.AuthHttpHeaderUsername != "" {
|
||||
if cfg.AuthHttpHeaderUsername != "" && username == "" {
|
||||
username, usergroup = parseHttpHeaderForAuth(req)
|
||||
provider = "http-header"
|
||||
}
|
||||
|
||||
md := metadata.Pairs(
|
||||
"username", username,
|
||||
"usergroup", usergroup,
|
||||
)
|
||||
if len(cfg.AuthOAuth2Providers) > 0 && username == "" {
|
||||
username, usergroup, sid = parseOAuth2Cookie(req)
|
||||
provider = "oauth2"
|
||||
}
|
||||
|
||||
if cfg.AuthLocalUsers.Enabled && username == "" {
|
||||
username, usergroup, sid = parseLocalUserCookie(req)
|
||||
provider = "local"
|
||||
}
|
||||
|
||||
md := metadata.New(map[string]string{
|
||||
"username": username,
|
||||
"usergroup": usergroup,
|
||||
"provider": provider,
|
||||
"sid": sid,
|
||||
})
|
||||
|
||||
log.Tracef("api request metadata: %+v", md)
|
||||
|
||||
return md
|
||||
}
|
||||
|
||||
func forwardResponseHandler(ctx context.Context, w http.ResponseWriter, msg protoreflect.ProtoMessage) error {
|
||||
md, ok := runtime.ServerMetadataFromContext(ctx)
|
||||
|
||||
if !ok {
|
||||
log.Warn("Could not get ServerMetadata from context")
|
||||
return nil
|
||||
}
|
||||
|
||||
forwardResponseHandlerLoginLocalUser(md.HeaderMD, w)
|
||||
forwardResponseHandlerLogout(md.HeaderMD, w)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func forwardResponseHandlerLogout(md metadata.MD, w http.ResponseWriter) {
|
||||
if getMetadataKeyOrEmpty(md, "logout-provider") != "" {
|
||||
sid := getMetadataKeyOrEmpty(md, "logout-sid")
|
||||
|
||||
delete(registeredStates, sid)
|
||||
http.SetCookie(
|
||||
w,
|
||||
&http.Cookie{
|
||||
Name: "olivetin-sid-oauth",
|
||||
MaxAge: 31556952, // 1 year
|
||||
Value: "",
|
||||
HttpOnly: true,
|
||||
Path: "/",
|
||||
},
|
||||
)
|
||||
|
||||
deleteLocalUserSession("local", sid)
|
||||
|
||||
http.SetCookie(
|
||||
w,
|
||||
&http.Cookie{
|
||||
Name: "olivetin-sid-local",
|
||||
MaxAge: 31556952, // 1 year
|
||||
Value: "",
|
||||
HttpOnly: true,
|
||||
Path: "/",
|
||||
},
|
||||
)
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
// We cannot send a HTTP redirect here, because we don't have access to req.
|
||||
w.Write([]byte("<script>window.location.href = '/';</script>"))
|
||||
}
|
||||
}
|
||||
|
||||
func getMetadataKeyOrEmpty(md metadata.MD, key string) string {
|
||||
mdValues := md.Get(key)
|
||||
|
||||
if len(mdValues) > 0 {
|
||||
return mdValues[0]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func SetGlobalRestConfig(config *config.Config) {
|
||||
cfg = config
|
||||
}
|
||||
@@ -74,8 +151,10 @@ func SetGlobalRestConfig(config *config.Config) {
|
||||
func startRestAPIServer(globalConfig *config.Config) error {
|
||||
cfg = globalConfig
|
||||
|
||||
loadUserSessions()
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"address": cfg.ListenAddressGrpcActions,
|
||||
"address": cfg.ListenAddressRestActions,
|
||||
}).Info("Starting REST API")
|
||||
|
||||
mux := newMux()
|
||||
@@ -87,6 +166,7 @@ func newMux() *runtime.ServeMux {
|
||||
// The MarshalOptions set some important compatibility settings for the webui. See below.
|
||||
mux := runtime.NewServeMux(
|
||||
runtime.WithMetadata(parseRequestMetadata),
|
||||
runtime.WithForwardResponseOption(forwardResponseHandler),
|
||||
runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.HTTPBodyMarshaler{
|
||||
Marshaler: &runtime.JSONPb{
|
||||
MarshalOptions: protojson.MarshalOptions{
|
||||
@@ -101,7 +181,7 @@ func newMux() *runtime.ServeMux {
|
||||
|
||||
opts := []grpc.DialOption{grpc.WithInsecure()}
|
||||
|
||||
err := gw.RegisterOliveTinApiServiceHandlerFromEndpoint(ctx, mux, cfg.ListenAddressGrpcActions, opts)
|
||||
err := apiv1.RegisterOliveTinApiServiceHandlerFromEndpoint(ctx, mux, cfg.ListenAddressGrpcActions, opts)
|
||||
|
||||
if err != nil {
|
||||
log.Panicf("Could not register REST API Handler %v", err)
|
||||
177
service/internal/httpservers/restapi_auth.go
Normal file
177
service/internal/httpservers/restapi_auth.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package httpservers
|
||||
|
||||
import (
|
||||
"github.com/OliveTin/OliveTin/internal/config"
|
||||
"github.com/OliveTin/OliveTin/internal/filehelper"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var sessionStorageMutex sync.Mutex
|
||||
|
||||
type UserSession struct {
|
||||
Username string
|
||||
Expiry int64
|
||||
}
|
||||
|
||||
type SessionProvider struct {
|
||||
Sessions map[string]*UserSession
|
||||
}
|
||||
|
||||
type SessionStorage struct {
|
||||
Providers map[string]*SessionProvider
|
||||
}
|
||||
|
||||
var (
|
||||
sessionStorage *SessionStorage
|
||||
)
|
||||
|
||||
func registerSessionProviders() {
|
||||
sessionStorage = &SessionStorage{
|
||||
Providers: make(map[string]*SessionProvider),
|
||||
}
|
||||
|
||||
registerSessionProvider("local")
|
||||
registerSessionProvider("oauth2")
|
||||
}
|
||||
|
||||
func registerSessionProvider(provider string) {
|
||||
sessionStorage.Providers[provider] = &SessionProvider{
|
||||
Sessions: make(map[string]*UserSession),
|
||||
}
|
||||
}
|
||||
|
||||
func deleteLocalUserSession(provider string, sid string) {
|
||||
sessionStorageMutex.Lock()
|
||||
|
||||
deleteLocalUserSessionBatch(provider, sid)
|
||||
|
||||
sessionStorageMutex.Unlock()
|
||||
|
||||
saveUserSessions()
|
||||
}
|
||||
|
||||
func deleteLocalUserSessionBatch(provider string, sid string) {
|
||||
log.WithFields(log.Fields{
|
||||
"sid": sid,
|
||||
"provider": provider,
|
||||
}).Debug("Deleting user session")
|
||||
|
||||
if _, ok := sessionStorage.Providers[provider]; !ok {
|
||||
return
|
||||
}
|
||||
|
||||
delete(sessionStorage.Providers[provider].Sessions, sid)
|
||||
}
|
||||
|
||||
func registerUserSession(provider string, sid string, username string) {
|
||||
sessionStorageMutex.Lock()
|
||||
sessionStorage.Providers[provider].Sessions[sid] = &UserSession{
|
||||
Username: username,
|
||||
Expiry: time.Now().Unix() + 31556952, // 1 year
|
||||
}
|
||||
sessionStorageMutex.Unlock()
|
||||
|
||||
saveUserSessions()
|
||||
}
|
||||
|
||||
func saveUserSessions() {
|
||||
sessionStorageMutex.Lock()
|
||||
defer sessionStorageMutex.Unlock()
|
||||
|
||||
filename := filepath.Join(cfg.GetDir(), "sessions.db.yaml")
|
||||
|
||||
out, err := yaml.Marshal(sessionStorage)
|
||||
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
}).Errorf("Failed to marshal session data to %v", filename)
|
||||
return
|
||||
}
|
||||
|
||||
filehelper.WriteFile(filename, out)
|
||||
}
|
||||
|
||||
func loadUserSessions() {
|
||||
registerSessionProviders()
|
||||
|
||||
filename := filepath.Join(cfg.GetDir(), "sessions.db.yaml")
|
||||
|
||||
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filename)
|
||||
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
}).Errorf("Failed to read %v", filename)
|
||||
return
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(data, &sessionStorage)
|
||||
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
}).Error("Failed to unmarshal sessions.local.db")
|
||||
return
|
||||
}
|
||||
|
||||
deleteExpiredSessions()
|
||||
}
|
||||
|
||||
func deleteExpiredSessions() {
|
||||
sessionStorageMutex.Lock()
|
||||
|
||||
for provider, sessions := range sessionStorage.Providers {
|
||||
for sid, session := range sessions.Sessions {
|
||||
if session.Expiry < time.Now().Unix() {
|
||||
deleteLocalUserSessionBatch(provider, sid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sessionStorageMutex.Unlock()
|
||||
|
||||
saveUserSessions()
|
||||
}
|
||||
|
||||
func getUserFromSession(providerName string, sid string) *config.LocalUser {
|
||||
provider, ok := sessionStorage.Providers[providerName]
|
||||
|
||||
if !ok {
|
||||
log.WithFields(log.Fields{
|
||||
"provider": providerName,
|
||||
}).Warnf("Provider not found")
|
||||
return nil
|
||||
}
|
||||
|
||||
session, ok := provider.Sessions[sid]
|
||||
|
||||
if !ok {
|
||||
log.WithFields(log.Fields{
|
||||
"sid": sid,
|
||||
"provider": providerName,
|
||||
}).Warnf("Stale session")
|
||||
return nil
|
||||
}
|
||||
|
||||
user := cfg.FindUserByUsername(session.Username)
|
||||
|
||||
if user == nil {
|
||||
log.WithFields(log.Fields{
|
||||
"sid": sid,
|
||||
"provider": providerName,
|
||||
}).Warnf("User not found")
|
||||
return nil
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
54
service/internal/httpservers/restapi_auth_local.go
Normal file
54
service/internal/httpservers/restapi_auth_local.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package httpservers
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc/metadata"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func parseLocalUserCookie(req *http.Request) (string, string, string) {
|
||||
cookie, err := req.Cookie("olivetin-sid-local")
|
||||
|
||||
if err != nil {
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
cookieValue := cookie.Value
|
||||
|
||||
user := getUserFromSession("local", cookieValue)
|
||||
|
||||
if user == nil {
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
return user.Username, user.Usergroup, cookie.Value
|
||||
}
|
||||
|
||||
func forwardResponseHandlerLoginLocalUser(md metadata.MD, w http.ResponseWriter) error {
|
||||
setUsername := getMetadataKeyOrEmpty(md, "set-username")
|
||||
|
||||
if setUsername != "" {
|
||||
user := cfg.FindUserByUsername(setUsername)
|
||||
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
sid := uuid.NewString()
|
||||
registerUserSession("local", sid, user.Username)
|
||||
|
||||
http.SetCookie(
|
||||
w,
|
||||
&http.Cookie{
|
||||
Name: "olivetin-sid-local",
|
||||
Value: sid,
|
||||
MaxAge: 31556952, // 1 year
|
||||
HttpOnly: true,
|
||||
Path: "/",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
349
service/internal/httpservers/restapi_auth_oauth2.go
Normal file
349
service/internal/httpservers/restapi_auth_oauth2.go
Normal file
@@ -0,0 +1,349 @@
|
||||
package httpservers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/oauth2"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
registeredStates = make(map[string]*oauth2State)
|
||||
registeredProviders = make(map[string]*oauth2.Config)
|
||||
)
|
||||
|
||||
type oauth2State struct {
|
||||
providerConfig *oauth2.Config
|
||||
providerName string
|
||||
Username string
|
||||
Usergroup string
|
||||
}
|
||||
|
||||
func assignIfEmpty(target *string, value string) {
|
||||
if *target == "" {
|
||||
*target = value
|
||||
}
|
||||
}
|
||||
|
||||
func oauth2Init(cfg *config.Config) {
|
||||
for providerName, providerConfig := range cfg.AuthOAuth2Providers {
|
||||
completeProviderConfig(providerName, providerConfig)
|
||||
|
||||
newConfig := &oauth2.Config{
|
||||
ClientID: providerConfig.ClientID,
|
||||
ClientSecret: providerConfig.ClientSecret,
|
||||
Scopes: providerConfig.Scopes,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: providerConfig.AuthUrl,
|
||||
TokenURL: providerConfig.TokenUrl,
|
||||
},
|
||||
RedirectURL: cfg.AuthOAuth2RedirectURL,
|
||||
}
|
||||
|
||||
registeredProviders[providerName] = newConfig
|
||||
|
||||
log.Debugf("Dumping newly registered provider: %v = %+v", providerName, providerConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func completeProviderConfig(providerName string, providerConfig *config.OAuth2Provider) {
|
||||
dbConfig, ok := oauth2ProviderDatabase[providerName]
|
||||
|
||||
if ok {
|
||||
assignIfEmpty(&providerConfig.Name, dbConfig.Name)
|
||||
assignIfEmpty(&providerConfig.Title, dbConfig.Title)
|
||||
assignIfEmpty(&providerConfig.WhoamiUrl, dbConfig.WhoamiUrl)
|
||||
assignIfEmpty(&providerConfig.TokenUrl, dbConfig.TokenUrl)
|
||||
assignIfEmpty(&providerConfig.AuthUrl, dbConfig.AuthUrl)
|
||||
assignIfEmpty(&providerConfig.Icon, dbConfig.Icon)
|
||||
assignIfEmpty(&providerConfig.UsernameField, dbConfig.UsernameField)
|
||||
|
||||
if providerConfig.Scopes == nil {
|
||||
providerConfig.Scopes = dbConfig.Scopes
|
||||
}
|
||||
} else {
|
||||
log.Warnf("Provider not found in database: %v", providerName)
|
||||
}
|
||||
}
|
||||
|
||||
func getOAuth2Config(cfg *config.Config, providerName string) (*oauth2.Config, error) {
|
||||
config, ok := registeredProviders[providerName]
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Provider not found in config: %v", providerName)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func randString(nByte int) (string, error) {
|
||||
b := make([]byte, nByte)
|
||||
|
||||
if _, err := io.ReadFull(rand.Reader, b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return base64.URLEncoding.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
func setOAuthCallbackCookie(w http.ResponseWriter, r *http.Request, name, value string) {
|
||||
cookie := &http.Cookie{
|
||||
Name: name,
|
||||
Value: value,
|
||||
MaxAge: 31556952, // 1 year
|
||||
Secure: r.TLS != nil,
|
||||
HttpOnly: true,
|
||||
Path: "/",
|
||||
}
|
||||
|
||||
http.SetCookie(w, cookie)
|
||||
}
|
||||
|
||||
func handleOAuthLogin(w http.ResponseWriter, r *http.Request) {
|
||||
state, err := randString(16)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
providerName := r.URL.Query().Get("provider")
|
||||
provider, err := getOAuth2Config(cfg, providerName)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get provider config: %v %v", providerName, err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
registeredStates[state] = &oauth2State{
|
||||
providerConfig: provider,
|
||||
providerName: providerName,
|
||||
Username: "",
|
||||
}
|
||||
|
||||
setOAuthCallbackCookie(w, r, "olivetin-sid-oauth", state)
|
||||
|
||||
log.Infof("OAuth2 state: %v mapped to provider %v (found: %v), now redirecting", state, providerName, provider != nil)
|
||||
|
||||
http.Redirect(w, r, provider.AuthCodeURL(state), http.StatusFound)
|
||||
}
|
||||
|
||||
func checkOAuthCallbackCookie(w http.ResponseWriter, r *http.Request) (*oauth2State, string, bool) {
|
||||
cookie, err := r.Cookie("olivetin-sid-oauth")
|
||||
state := cookie.Value
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get state cookie: %v", err)
|
||||
|
||||
http.Error(w, "State not found", http.StatusBadRequest)
|
||||
return nil, state, false
|
||||
}
|
||||
|
||||
if r.URL.Query().Get("state") != state {
|
||||
log.Errorf("State mismatch: %v != %v", r.URL.Query().Get("state"), state)
|
||||
|
||||
http.Error(w, "State mismatch", http.StatusBadRequest)
|
||||
return nil, state, false
|
||||
}
|
||||
|
||||
registeredState, ok := registeredStates[state]
|
||||
|
||||
if !ok {
|
||||
log.Errorf("State not found in server: %v", state)
|
||||
|
||||
http.Error(w, "State not found in server", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
return registeredState, state, true
|
||||
}
|
||||
|
||||
type HttpClientSettings struct {
|
||||
Transport *http.Transport
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
func getOAuth2HttpClient(providerConfig *config.OAuth2Provider) *HttpClientSettings {
|
||||
config := &HttpClientSettings{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: providerConfig.InsecureSkipVerify},
|
||||
},
|
||||
Timeout: time.Duration(min(3, providerConfig.CallbackTimeout)) * time.Second,
|
||||
}
|
||||
|
||||
if providerConfig.CertBundlePath != "" {
|
||||
config.Transport.TLSClientConfig.RootCAs = getOAuthCertBundle(providerConfig)
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func getOAuthCertBundle(providerConfig *config.OAuth2Provider) *x509.CertPool {
|
||||
caCert, err := os.ReadFile(providerConfig.CertBundlePath)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("OAuth2 Cert Bundle - failed to read file: %v", err)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
caCertPool := x509.NewCertPool()
|
||||
|
||||
if ok := caCertPool.AppendCertsFromPEM(caCert); !ok {
|
||||
log.Errorf("OAuth2 Cert Bundle - failed to append certificates: %v", err)
|
||||
}
|
||||
|
||||
return caCertPool
|
||||
}
|
||||
|
||||
func handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
|
||||
log.Infof("OAuth2 Callback received")
|
||||
|
||||
registeredState, state, ok := checkOAuthCallbackCookie(w, r)
|
||||
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
code := r.FormValue("code")
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"state": state,
|
||||
"token-code": code,
|
||||
}).Debug("OAuth2 Token Code")
|
||||
|
||||
providerConfig := cfg.AuthOAuth2Providers[registeredState.providerName]
|
||||
|
||||
clientSettings := getOAuth2HttpClient(providerConfig)
|
||||
|
||||
exchangeClient := &http.Client{
|
||||
Transport: clientSettings.Transport,
|
||||
Timeout: clientSettings.Timeout,
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, exchangeClient)
|
||||
|
||||
tok, err := registeredState.providerConfig.Exchange(ctx, code)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Failed to exchange code: %v", err)
|
||||
http.Error(w, "Failed to exchange code", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
userInfoClient := &http.Client{
|
||||
Transport: &oauth2.Transport{
|
||||
Source: registeredState.providerConfig.TokenSource(ctx, tok),
|
||||
Base: clientSettings.Transport,
|
||||
},
|
||||
Timeout: clientSettings.Timeout,
|
||||
}
|
||||
|
||||
userinfo := getUserInfo(userInfoClient, cfg.AuthOAuth2Providers[registeredState.providerName])
|
||||
|
||||
registeredStates[state].Username = userinfo.Username
|
||||
registeredStates[state].Usergroup = userinfo.Usergroup
|
||||
|
||||
for k, v := range registeredStates {
|
||||
log.Debugf("states: %+v %+v", k, v)
|
||||
}
|
||||
|
||||
loginMessage := fmt.Sprintf("OAuth2 login complete for %v", registeredStates[state].Username)
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"state": state,
|
||||
}).Infof(loginMessage)
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
w.Write([]byte(loginMessage))
|
||||
}
|
||||
|
||||
type UserInfo struct {
|
||||
Username string
|
||||
Usergroup string
|
||||
}
|
||||
|
||||
func getUserInfo(client *http.Client, provider *config.OAuth2Provider) *UserInfo {
|
||||
ret := &UserInfo{}
|
||||
|
||||
res, err := client.Get(provider.WhoamiUrl)
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
log.Errorf("Failed to get user data: %v", res.StatusCode)
|
||||
return ret
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
|
||||
contents, err := io.ReadAll(res.Body)
|
||||
|
||||
var userData map[string]interface{}
|
||||
|
||||
err = json.Unmarshal([]byte(contents), &userData)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Failed to unmarshal user data: %v", err)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
ret.Username = getDataField(userData, provider.UsernameField)
|
||||
ret.Usergroup = getDataField(userData, provider.UserGroupField)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func getDataField(data map[string]interface{}, field string) string {
|
||||
if field == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
val, ok := data[field]
|
||||
|
||||
if !ok {
|
||||
log.Errorf("Failed to get field from user data: %v / %v", data, field)
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
return val.(string)
|
||||
}
|
||||
|
||||
func parseOAuth2Cookie(r *http.Request) (string, string, string) {
|
||||
cookie, err := r.Cookie("olivetin-sid-oauth")
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("Failed to read OAuth2 cookie: %v", err)
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
if cookie.Value == "" {
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
serverState, found := registeredStates[cookie.Value]
|
||||
|
||||
if !found {
|
||||
log.WithFields(log.Fields{
|
||||
"sid": cookie.Value,
|
||||
"provider": "oauth2",
|
||||
}).Warnf("Stale session")
|
||||
|
||||
return "", "", cookie.Value
|
||||
}
|
||||
|
||||
log.Debugf("Found OAuth2 state: %+v", serverState)
|
||||
|
||||
return serverState.Username, serverState.Usergroup, cookie.Value
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package httpservers
|
||||
|
||||
import (
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
"golang.org/x/oauth2/endpoints"
|
||||
)
|
||||
|
||||
var oauth2ProviderDatabase = map[string]config.OAuth2Provider{
|
||||
"github": {
|
||||
Title: "GitHub",
|
||||
Name: "github",
|
||||
Icon: "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1em\" height=\"1em\" viewBox=\"0 0 128 128\"><g fill=\"currentColor\"><path fill-rule=\"evenodd\" d=\"M64 5.103c-33.347 0-60.388 27.035-60.388 60.388c0 26.682 17.303 49.317 41.297 57.303c3.017.56 4.125-1.31 4.125-2.905c0-1.44-.056-6.197-.082-11.243c-16.8 3.653-20.345-7.125-20.345-7.125c-2.747-6.98-6.705-8.836-6.705-8.836c-5.48-3.748.413-3.67.413-3.67c6.063.425 9.257 6.223 9.257 6.223c5.386 9.23 14.127 6.562 17.573 5.02c.542-3.903 2.107-6.568 3.834-8.076c-13.413-1.525-27.514-6.704-27.514-29.843c0-6.593 2.36-11.98 6.223-16.21c-.628-1.52-2.695-7.662.584-15.98c0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033c11.526-7.813 16.59-6.19 16.59-6.19c3.287 8.317 1.22 14.46.593 15.98c3.872 4.23 6.215 9.617 6.215 16.21c0 23.194-14.127 28.3-27.574 29.796c2.167 1.874 4.097 5.55 4.097 11.183c0 8.08-.07 14.583-.07 16.572c0 1.607 1.088 3.49 4.148 2.897c23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z\" clip-rule=\"evenodd\"/><path d=\"M26.484 91.806c-.133.3-.605.39-1.035.185c-.44-.196-.685-.605-.543-.906c.13-.31.603-.395 1.04-.188c.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28c-.396-.42-.47-.983-.177-1.254c.298-.266.844-.14 1.24.28c.394.426.472.984.17 1.255zm2.382 3.477c-.37.258-.976.017-1.35-.52c-.37-.538-.37-1.183.01-1.44c.373-.258.97-.025 1.35.507c.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23c-.527-.487-.674-1.18-.343-1.544c.336-.366 1.045-.264 1.564.23c.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486c-.683-.207-1.13-.76-.99-1.238c.14-.477.823-.7 1.512-.485c.683.206 1.13.756.988 1.237m4.943.361c.017.498-.563.91-1.28.92c-.723.017-1.308-.387-1.315-.877c0-.503.568-.91 1.29-.924c.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117c-.7.13-1.35-.172-1.44-.653c-.086-.498.422-.997 1.122-1.126c.714-.123 1.354.17 1.444.663zm0 0\"/></g></svg>",
|
||||
WhoamiUrl: "https://api.github.com/user",
|
||||
TokenUrl: endpoints.GitHub.TokenURL,
|
||||
AuthUrl: endpoints.GitHub.AuthURL,
|
||||
Scopes: []string{"profile", "email"},
|
||||
UsernameField: "login",
|
||||
},
|
||||
"google": {
|
||||
Icon: "google",
|
||||
WhoamiUrl: "https://www.googleapis.com/oauth2/v3/userinfo",
|
||||
TokenUrl: endpoints.Google.TokenURL,
|
||||
AuthUrl: endpoints.Google.AuthURL,
|
||||
Scopes: []string{"profile", "email"},
|
||||
},
|
||||
}
|
||||
@@ -56,6 +56,10 @@ func StartSingleHTTPFrontend(cfg *config.Config) {
|
||||
websocket.HandleWebsocket(w, r)
|
||||
})
|
||||
|
||||
mux.HandleFunc("/oauth/login", handleOAuthLogin)
|
||||
|
||||
mux.HandleFunc("/oauth/callback", handleOAuthCallback)
|
||||
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
logDebugRequest(cfg, "ui ", r)
|
||||
|
||||
@@ -73,6 +77,8 @@ func StartSingleHTTPFrontend(cfg *config.Config) {
|
||||
})
|
||||
}
|
||||
|
||||
oauth2Init(cfg)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: cfg.ListenAddressSingleHTTPFrontend,
|
||||
Handler: mux,
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
installationinfo "github.com/OliveTin/OliveTin/internal/installationinfo"
|
||||
sv "github.com/OliveTin/OliveTin/internal/stringvariables"
|
||||
updatecheck "github.com/OliveTin/OliveTin/internal/updatecheck"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -29,6 +29,13 @@ type webUISettings struct {
|
||||
PageTitle string
|
||||
SectionNavigationStyle string
|
||||
DefaultIconForBack string
|
||||
SshFoundKey string
|
||||
SshFoundConfig string
|
||||
EnableCustomJs bool
|
||||
AuthLoginUrl string
|
||||
AuthLocalLogin bool
|
||||
AuthOAuth2Providers []publicOAuth2Provider
|
||||
AdditionalLinks []*config.NavigationLink
|
||||
}
|
||||
|
||||
func findWebuiDir() string {
|
||||
@@ -85,7 +92,7 @@ func setupCustomWebuiDir() {
|
||||
func generateThemeCss(w http.ResponseWriter, r *http.Request) {
|
||||
themeCssFilename := path.Join(findCustomWebuiDir(), "themes", cfg.ThemeName, "theme.css")
|
||||
|
||||
if !customThemeCssRead {
|
||||
if !customThemeCssRead || cfg.ThemeCacheDisabled {
|
||||
customThemeCssRead = true
|
||||
|
||||
if _, err := os.Stat(themeCssFilename); err == nil {
|
||||
@@ -100,19 +107,47 @@ func generateThemeCss(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(customThemeCss)
|
||||
}
|
||||
|
||||
type publicOAuth2Provider struct {
|
||||
Name string
|
||||
Title string
|
||||
Icon string
|
||||
}
|
||||
|
||||
func buildPublicOAuth2ProvidersList(cfg *config.Config) []publicOAuth2Provider {
|
||||
var publicProviders []publicOAuth2Provider
|
||||
|
||||
for _, provider := range cfg.AuthOAuth2Providers {
|
||||
publicProviders = append(publicProviders, publicOAuth2Provider{
|
||||
Name: provider.Name,
|
||||
Title: provider.Title,
|
||||
Icon: provider.Icon,
|
||||
})
|
||||
}
|
||||
|
||||
return publicProviders
|
||||
}
|
||||
|
||||
func generateWebUISettings(w http.ResponseWriter, r *http.Request) {
|
||||
jsonRet, _ := json.Marshal(webUISettings{
|
||||
Rest: cfg.ExternalRestAddress + "/api/",
|
||||
ShowFooter: cfg.ShowFooter,
|
||||
ShowNavigation: cfg.ShowNavigation,
|
||||
ShowNewVersions: cfg.ShowNewVersions,
|
||||
AvailableVersion: updatecheck.AvailableVersion,
|
||||
CurrentVersion: updatecheck.CurrentVersion,
|
||||
AvailableVersion: installationinfo.Runtime.AvailableVersion,
|
||||
CurrentVersion: installationinfo.Build.Version,
|
||||
PageTitle: cfg.PageTitle,
|
||||
SectionNavigationStyle: cfg.SectionNavigationStyle,
|
||||
DefaultIconForBack: cfg.DefaultIconForBack,
|
||||
SshFoundKey: installationinfo.Runtime.SshFoundKey,
|
||||
SshFoundConfig: installationinfo.Runtime.SshFoundConfig,
|
||||
EnableCustomJs: cfg.EnableCustomJs,
|
||||
AuthLoginUrl: cfg.AuthLoginUrl,
|
||||
AuthLocalLogin: cfg.AuthLocalUsers.Enabled,
|
||||
AuthOAuth2Providers: buildPublicOAuth2ProvidersList(cfg),
|
||||
AdditionalLinks: cfg.AdditionalNavigationLinks,
|
||||
})
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
_, err := w.Write([]byte(jsonRet))
|
||||
|
||||
if err != nil {
|
||||
@@ -128,11 +163,24 @@ func startWebUIServer(cfg *config.Config) {
|
||||
setupCustomWebuiDir()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/", http.FileServer(http.Dir(findWebuiDir())))
|
||||
mux.Handle("/custom-webui/", http.StripPrefix("/custom-webui/", http.FileServer(http.Dir(findCustomWebuiDir()))))
|
||||
mux.HandleFunc("/theme.css", generateThemeCss)
|
||||
mux.HandleFunc("/webUiSettings.json", generateWebUISettings)
|
||||
|
||||
webuiDir := findWebuiDir()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
dirName := path.Dir(r.URL.Path)
|
||||
|
||||
// Mangle requests for any path like /logs or /config to load the webui index.html
|
||||
if path.Ext(r.URL.Path) == "" && r.URL.Path != "/" {
|
||||
log.Debugf("Mangling request for %s to /index.html", r.URL.Path)
|
||||
|
||||
http.ServeFile(w, r, path.Join(webuiDir, "index.html"))
|
||||
} else {
|
||||
http.StripPrefix(dirName, http.FileServer(http.Dir(webuiDir))).ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: cfg.ListenAddressWebUI,
|
||||
Handler: mux,
|
||||
@@ -14,5 +14,5 @@ func TestGetWebuiDir(t *testing.T) {
|
||||
|
||||
dir := findWebuiDir()
|
||||
|
||||
assert.Equal(t, "./webui", dir, "Finding the webui dir")
|
||||
assert.Equal(t, "../webui/", dir, "Finding the webui dir")
|
||||
}
|
||||
@@ -18,7 +18,9 @@ type runtimeInfo struct {
|
||||
LastBrowserUserAgent string
|
||||
User string
|
||||
Uid string
|
||||
FoundSshKey string
|
||||
SshFoundKey string
|
||||
SshFoundConfig string
|
||||
AvailableVersion string
|
||||
}
|
||||
|
||||
var Runtime = &runtimeInfo{
|
||||
@@ -28,20 +30,42 @@ var Runtime = &runtimeInfo{
|
||||
OSReleasePrettyName: getOsReleasePrettyName(),
|
||||
User: os.Getenv("USER"),
|
||||
Uid: os.Getenv("UID"),
|
||||
SshFoundKey: searchForSshKey(),
|
||||
SshFoundConfig: searchForSshConfig(),
|
||||
}
|
||||
|
||||
func refreshRuntimeInfo() {
|
||||
Runtime.FoundSshKey = searchForSshKey()
|
||||
func fileExists(path string) bool {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func searchForSshKey() string {
|
||||
path, _ := filepath.Abs(path.Join(os.Getenv("HOME"), ".ssh/id_rsa"))
|
||||
if fileExists("/config/ssh/id_rsa") {
|
||||
return "/config/ssh/id_rsa"
|
||||
}
|
||||
|
||||
return searchForHomeFile(".ssh/id_rsa")
|
||||
}
|
||||
|
||||
func searchForSshConfig() string {
|
||||
if fileExists("/config/ssh/config") {
|
||||
return "/config/ssh/config"
|
||||
}
|
||||
|
||||
return searchForHomeFile(".ssh/config")
|
||||
}
|
||||
|
||||
func searchForHomeFile(file string) string {
|
||||
path, _ := filepath.Abs(path.Join(os.Getenv("HOME"), file))
|
||||
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path
|
||||
}
|
||||
|
||||
return "none-found at " + path
|
||||
return "not found at " + path
|
||||
}
|
||||
|
||||
func isInContainer() bool {
|
||||
@@ -43,8 +43,6 @@ func configToSosreport(cfg *config.Config) *sosReportConfig {
|
||||
}
|
||||
|
||||
func GetSosReport() string {
|
||||
refreshRuntimeInfo()
|
||||
|
||||
ret := ""
|
||||
|
||||
ret += "### SOSREPORT START (copy all text to SOSREPORT END)\n"
|
||||
@@ -107,12 +107,10 @@ func exec(instant time.Time, action *config.Action, cfg *config.Config, ex *exec
|
||||
}).Infof("Executing action from calendar")
|
||||
|
||||
req := &executor.ExecutionRequest{
|
||||
Action: action,
|
||||
Cfg: cfg,
|
||||
Tags: []string{"calendar"},
|
||||
AuthenticatedUser: &acl.AuthenticatedUser{
|
||||
Username: "calendar",
|
||||
},
|
||||
Action: action,
|
||||
Cfg: cfg,
|
||||
Tags: []string{},
|
||||
AuthenticatedUser: acl.UserFromSystem(cfg, "calendar"),
|
||||
}
|
||||
|
||||
ex.ExecRequest(req)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user