Merge branch 'main' of github.com:OliveTin/OliveTin

This commit is contained in:
jamesread 2023-09-23 06:43:52 +01:00
commit 3ea14f1353
60 changed files with 2713 additions and 460 deletions

44
.air.toml Normal file
View File

@ -0,0 +1,44 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./OliveTin"
cmd = "go build -o OliveTin github.com/OliveTin/OliveTin/cmd/OliveTin"
delay = 1
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = true
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true

View File

@ -30,7 +30,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v3
with:
go-version: '>=1.18.0'
go-version: '^1.18.0'
cache: true
- name: grpc

View File

@ -19,9 +19,10 @@ jobs:
uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@v2
uses: actions/setup-go@v3
with:
go-version: '^1.16.0'
go-version: '^1.18.0'
cache: true
- name: deps
run: make grpc

1
.gitignore vendored
View File

@ -9,3 +9,4 @@ reports
releases/
dist/
installation-id.txt
tmp/

View File

@ -236,4 +236,3 @@ release:
- [Ask for help and chat with others users in the Discord community](https://discord.gg/jhYWWpNJ3v)
Thanks for your interest in OliveTin!

10
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,10 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files

View File

@ -8,16 +8,16 @@ message Action {
string id = 1;
string title = 2;
string icon = 3;
bool canExec = 4;
bool can_exec = 4;
repeated ActionArgument arguments = 5;
bool popup_on_start = 6;
}
message ActionArgument {
string name = 1;
string title = 2;
string type = 3;
string defaultValue = 4;
string default_value = 4;
repeated ActionArgumentChoice choices = 5;
@ -44,9 +44,11 @@ message GetDashboardComponentsResponse {
message GetDashboardComponentsRequest {}
message StartActionRequest {
string actionName = 1;
string action_name = 1;
repeated StartActionArgument arguments = 2;
string uuid = 3;
}
message StartActionArgument {
@ -55,22 +57,28 @@ message StartActionArgument {
}
message StartActionResponse {
LogEntry logEntry = 1;
string execution_uuid = 2;
}
message GetLogsRequest{};
message LogEntry {
string datetime = 1;
string actionTitle = 2;
string datetime_started = 1;
string action_title = 2;
string stdout = 3;
string stderr = 4;
bool timedOut = 5;
int32 exitCode = 6;
bool timed_out = 5;
int32 exit_code = 6;
string user = 7;
string userClass = 8;
string actionIcon = 9;
string user_class = 8;
string action_icon = 9;
repeated string tags = 10;
string execution_uuid = 11;
string datetime_finished = 12;
string uuid = 13;
bool execution_started = 14;
bool execution_finished = 15;
bool blocked = 16;
}
message GetLogsResponse {
@ -87,10 +95,26 @@ message ValidateArgumentTypeResponse {
string description = 2;
}
message WatchExecutionRequest {
string execution_uuid = 1;
}
message WatchExecutionUpdate {
string update = 1;
}
message ExecutionStatusRequest {
string execution_uuid = 1;
}
message ExecutionStatusResponse {
LogEntry log_entry = 1;
}
message WhoAmIRequest {}
message WhoAmIResponse {
string authenticatedUser = 1;
string authenticated_user = 1;
}
message SosReportRequest {}
@ -99,7 +123,7 @@ message SosReportResponse {
string alert = 1;
}
service OliveTinApi {
service OliveTinApiService {
rpc GetDashboardComponents(GetDashboardComponentsRequest) returns (GetDashboardComponentsResponse) {
option (google.api.http) = {
get: "/api/GetDashboardComponents"
@ -113,6 +137,13 @@ service OliveTinApi {
};
}
rpc ExecutionStatus(ExecutionStatusRequest) returns (ExecutionStatusResponse) {
option (google.api.http) = {
post: "/api/ExecutionStatus"
body: "*"
};
}
rpc GetLogs(GetLogsRequest) returns (GetLogsResponse) {
option (google.api.http) = {
get: "/api/GetLogs"

View File

@ -111,4 +111,3 @@ actions:
```
A full example config can be found at in this repository - [config.yaml](https://github.com/OliveTin/OliveTin/blob/main/config.yaml).

View File

@ -17,4 +17,3 @@ plugins:
# - name: openapiv2
# out: reports/openapiv2

View File

@ -11,6 +11,7 @@ import (
"github.com/OliveTin/OliveTin/internal/oncron"
"github.com/OliveTin/OliveTin/internal/onstartup"
updatecheck "github.com/OliveTin/OliveTin/internal/updatecheck"
"github.com/OliveTin/OliveTin/internal/websocket"
"github.com/OliveTin/OliveTin/internal/httpservers"
@ -110,6 +111,7 @@ func main() {
log.Debugf("Config: %+v", cfg)
executor := executor.DefaultExecutor()
executor.AddListener(websocket.ExecutionListener)
go onstartup.Execute(cfg, executor)
go oncron.Schedule(cfg, executor)

View File

@ -13,13 +13,18 @@ actions:
# This will run a simple script that you create.
- title: Run backup script
shell: /opt/backupScript.sh
maxConcurrent: 1
icon: backup
- title: date
shell: date
# This will send 1 ping (-c 1)
# Docs: https://docs.olivetin.app/action-ping.html
- title: Ping host
shell: ping {{ host }} -c {{ count }}
icon: ping
timeout: 100
arguments:
- name: host
title: host
@ -76,4 +81,3 @@ actions:
shell: sleep 5
timeout: 5
icon: "&#x1F62A"

93
go.mod
View File

@ -3,41 +3,45 @@ module github.com/OliveTin/OliveTin
go 1.18
require (
github.com/bufbuild/buf v1.15.1
github.com/bufbuild/buf v1.26.0
github.com/fsnotify/fsnotify v1.6.0
github.com/fzipp/gocyclo v0.6.0
github.com/go-critic/go-critic v0.7.0
github.com/go-critic/go-critic v0.8.2
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/google/uuid v1.3.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2
github.com/gorilla/websocket v1.4.1
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.2
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.9.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.15.0
github.com/stretchr/testify v1.8.2
golang.org/x/exp v0.0.0-20230307190834-24139beb5833
google.golang.org/grpc v1.53.0
github.com/stretchr/testify v1.8.4
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1
google.golang.org/genproto/googleapis/api v0.0.0-20230807174057-1744710a1577
google.golang.org/grpc v1.57.0
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0
google.golang.org/protobuf v1.30.0
google.golang.org/protobuf v1.31.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Microsoft/go-winio v0.6.0 // indirect
github.com/bufbuild/connect-go v1.5.2 // indirect
github.com/bufbuild/protocompile v0.5.1 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/bufbuild/connect-go v1.10.0 // indirect
github.com/bufbuild/connect-opentelemetry-go v0.4.0 // indirect
github.com/bufbuild/protocompile v0.6.0 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/cristalhq/acmd v0.11.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/cli v23.0.1+incompatible // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/docker v23.0.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.7.0 // indirect
github.com/docker/cli v24.0.5+incompatible // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/docker v24.0.5+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.0 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/fgprof v0.9.3 // indirect
github.com/go-chi/chi/v5 v5.0.8 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-chi/chi/v5 v5.0.10 // indirect
github.com/go-logr/logr v1.2.4 // 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
@ -47,25 +51,24 @@ require (
github.com/go-toolsmith/pkgload v1.2.2 // indirect
github.com/go-toolsmith/strparse v1.1.0 // indirect
github.com/go-toolsmith/typep v1.1.0 // indirect
github.com/gofrs/flock v0.8.1 // indirect
github.com/gofrs/uuid/v5 v5.0.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/golang/glog v1.1.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-containerregistry v0.13.0 // indirect
github.com/google/pprof v0.0.0-20230228050547-1710fef4ab10 // indirect
github.com/google/go-containerregistry v0.16.1 // indirect
github.com/google/pprof v0.0.0-20230808223545-4887780b67fb // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84 // indirect
github.com/klauspost/compress v1.16.0 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc2 // indirect
github.com/opencontainers/image-spec v1.1.0-rc4 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pkg/errors v0.9.1 // indirect
@ -75,29 +78,33 @@ require (
github.com/quasilyte/gogrep v0.5.0 // indirect
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
github.com/rs/cors v1.8.3 // indirect
github.com/rs/cors v1.9.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/cobra v1.6.1 // indirect
github.com/spf13/cobra v1.7.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
go.opentelemetry.io/otel v1.14.0 // indirect
go.opentelemetry.io/otel/sdk v1.14.0 // indirect
go.opentelemetry.io/otel/trace v1.14.0 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
go.uber.org/zap v1.24.0 // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20230213192124-5e25df0256eb // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/tools v0.7.0 // indirect
google.golang.org/genproto v0.0.0-20230223222841-637eb2293923 // indirect
github.com/tetratelabs/wazero v1.4.0 // indirect
github.com/vbatts/tar-split v0.11.5 // indirect
go.opentelemetry.io/otel v1.16.0 // indirect
go.opentelemetry.io/otel/metric v1.16.0 // indirect
go.opentelemetry.io/otel/sdk v1.16.0 // indirect
go.opentelemetry.io/otel/trace v1.16.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.25.0 // indirect
golang.org/x/crypto v0.12.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20230809150735-7b3493d9a819 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.14.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/term v0.11.0 // indirect
golang.org/x/text v0.12.0 // indirect
golang.org/x/tools v0.12.0 // indirect
google.golang.org/genproto v0.0.0-20230807174057-1744710a1577 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230807174057-1744710a1577 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
)

193
go.sum
View File

@ -40,15 +40,17 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
github.com/bufbuild/buf v1.15.1 h1:v7sK2uMEsGX4Z2hvu+xiMheH3C3AKBGfxPBgdUZYDQ8=
github.com/bufbuild/buf v1.15.1/go.mod h1:TQeGKam1QMfHy/xsSnnMpxN3JK5HBb6aNvZj4m52gkE=
github.com/bufbuild/connect-go v1.5.2 h1:G4EZd5gF1U1ZhhbVJXplbuUnfKpBZ5j5izqIwu2g2W8=
github.com/bufbuild/connect-go v1.5.2/go.mod h1:GmMJYR6orFqD0Y6ZgX8pwQ8j9baizDrIQMm1/a6LnHk=
github.com/bufbuild/protocompile v0.5.1 h1:mixz5lJX4Hiz4FpqFREJHIXLfaLBntfaJv1h+/jS+Qg=
github.com/bufbuild/protocompile v0.5.1/go.mod h1:G5iLmavmF4NsYtpZFvE3B/zFch2GIY8+wjsYLR/lc40=
github.com/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/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
github.com/bufbuild/buf v1.26.0 h1:rB8pErEkxdUNhVZ3+ClFEoJRaa+o2FjEecCWL2S3Qs4=
github.com/bufbuild/buf v1.26.0/go.mod h1:UMPncXMWgrmIM+0QpwTEwjNr2SA0z2YIVZZsmNflvB4=
github.com/bufbuild/connect-go v1.10.0 h1:QAJ3G9A1OYQW2Jbk3DeoJbkCxuKArrvZgDt47mjdTbg=
github.com/bufbuild/connect-go v1.10.0/go.mod h1:CAIePUgkDR5pAFaylSMtNK45ANQjp9JvpluG20rhpV8=
github.com/bufbuild/connect-opentelemetry-go v0.4.0 h1:6JAn10SNqlQ/URhvRNGrIlczKw1wEXknBUUtmWqOiak=
github.com/bufbuild/connect-opentelemetry-go v0.4.0/go.mod h1:nwPXYoDOoc2DGyKE/6pT1Q9MPSi2Et2e6BieMD0l6WU=
github.com/bufbuild/protocompile v0.6.0 h1:Uu7WiSQ6Yj9DbkdnOe7U4mNKp58y9WDMKDn28/ZlunY=
github.com/bufbuild/protocompile v0.6.0/go.mod h1:YNP35qEYoYGme7QMtz5SBCoN4kL4g12jTtjuzRNdjpE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
@ -57,7 +59,8 @@ 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/stargz-snapshotter/estargz v0.12.1 h1:+7nYmHJb0tEkcRaAW+MHqoKaJYZmkikupxCqVtmPuY0=
github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
@ -66,14 +69,14 @@ github.com/cristalhq/acmd v0.11.1/go.mod h1:LG5oa43pE/BbxtfMoImHCQN++0Su7dzipdgB
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/cli v23.0.1+incompatible h1:LRyWITpGzl2C9e9uGxzisptnxAn1zfZKXy13Ul2Q5oM=
github.com/docker/cli v23.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v23.0.3+incompatible h1:9GhVsShNWz1hO//9BNg/dpMnZW25KydO4wtVxWAIbho=
github.com/docker/docker v23.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
github.com/docker/cli v24.0.5+incompatible h1:WeBimjvS0eKdH4Ygx+ihVq1Q++xg36M/rMi4aXAvodc=
github.com/docker/cli v24.0.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v24.0.5+incompatible h1:WmgcE4fxyI6EEXxBRxsHnZXrO1pQ3smi0k/jho4HLeY=
github.com/docker/docker v24.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.0 h1:YQFtbBQb4VrpoPxhFuzEBPQ9E16qz5SpHLS+uswaCp8=
github.com/docker/docker-credential-helpers v0.8.0/go.mod h1:UGFXcuoQ5TxPiB54nHOZ32AWRqQdECoh/Mg0AlEYb40=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@ -91,16 +94,16 @@ 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.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-critic/go-critic v0.7.0 h1:tqbKzB8pqi0NsRZ+1pyU4aweAF7A7QN0Pi4Q02+rYnQ=
github.com/go-critic/go-critic v0.7.0/go.mod h1:moYzd7GdVXE2C2hYTwd7h0CPcqlUeclsyBRwMa38v64=
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-critic/go-critic v0.8.2 h1:mekhZ9jw5NBEj3I8o/EywXw5zBfGAJuMo4VVVjtxF80=
github.com/go-critic/go-critic v0.8.2/go.mod h1:nZPlrtVfOuLOe8GpvWTfcMzfkG0QVZWAziAeXpivfQo=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8=
@ -122,7 +125,6 @@ github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJ
github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus=
github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@ -130,8 +132,8 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69
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/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo=
github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -173,8 +175,8 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-containerregistry v0.13.0 h1:y1C7Z3e149OJbOPDBxLYR8ITPz8dTKqQwjErKVHJC8k=
github.com/google/go-containerregistry v0.13.0/go.mod h1:J9FQ+eSS4a1aC2GNZxvNpbWhgp0487v+cgiilB4FqDo=
github.com/google/go-containerregistry v0.16.1 h1:rUEt426sR6nyrL3gt+18ibRcvYpKYdpsa5ZW7MA08dQ=
github.com/google/go-containerregistry v0.16.1/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@ -189,8 +191,8 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/google/pprof v0.0.0-20230228050547-1710fef4ab10 h1:CqYfpuYIjnlNxM3msdyPRKabhXZWbKjf3Q8BWROFBso=
github.com/google/pprof v0.0.0-20230228050547-1710fef4ab10/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk=
github.com/google/pprof v0.0.0-20230808223545-4887780b67fb h1:oqpb3Cwpc7EOml5PVGMYbSGmwNui2R7i8IW83gs4W0c=
github.com/google/pprof v0.0.0-20230808223545-4887780b67fb/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
@ -198,8 +200,10 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 h1:gDLXvp5S9izjldquuoAhDzccbskOL6tDC5jMSyx3zxE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2/go.mod h1:7pdNwVWBBHGiCxa9lAszqCJMbfTISJ7oMftp8+UGV08=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.2 h1:dygLcbEBA+t/P7ck6a8AkXv6juQ4cK0RHBoh32jxhHM=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.2/go.mod h1:Ap9RLCIJVtgQg1/BBgVEfypOAySvvlcpcVQkSzJCH4Y=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
@ -207,7 +211,6 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84 h1:2uT3aivO7NVpUPGcQX7RbHijHMyWix/yCnIrCWc+5co=
@ -217,10 +220,10 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@ -233,15 +236,15 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA=
github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034=
github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ=
github.com/opencontainers/image-spec v1.1.0-rc4 h1:oOxKUJWnFC4YGHCCMNql1x4YaDfYBTS5Y4x/Cgeo1E0=
github.com/opencontainers/image-spec v1.1.0-rc4/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
@ -266,18 +269,18 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rs/cors v1.8.3 h1:O+qNyWn7Z+F9M0ILBHgMVPuB1xTOucVd5gtaYyXBpRo=
github.com/rs/cors v1.8.3/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE=
github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
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=
@ -294,11 +297,14 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/vbatts/tar-split v0.11.2 h1:Via6XqJr0hceW4wff3QRzD5gAk/tatMw/4ZA7cTlIME=
github.com/tetratelabs/wazero v1.4.0 h1:9/MirYvmkJ/zSUOygKY/ia3t+e+RqIZXKbylIby1WYk=
github.com/tetratelabs/wazero v1.4.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A=
github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts=
github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -309,19 +315,22 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM=
go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU=
go.opentelemetry.io/otel/sdk v1.14.0 h1:PDCppFRDq8A1jL9v6KMI6dYesaq+DFcDZvjsoGvxGzY=
go.opentelemetry.io/otel/sdk v1.14.0/go.mod h1:bwIC5TjrNG6QDCHNWvW4HLHtUQ4I+VQDsnjhvyZCALM=
go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M=
go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo=
go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4=
go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE=
go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4=
go.opentelemetry.io/otel/sdk/metric v0.39.0 h1:Kun8i1eYf48kHH83RucG93ffz0zGV1sh46FAScOTuDI=
go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
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.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c=
go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -329,8 +338,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -341,12 +350,12 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20230307190834-24139beb5833 h1:SChBja7BCQewoTAU7IgvucQKMIXrEpFxNMs0spT3/5s=
golang.org/x/exp v0.0.0-20230307190834-24139beb5833/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/exp/typeparams v0.0.0-20230213192124-5e25df0256eb h1:WGs/bGIWYyAY5PVgGGMXqGGCxSJz4fpoUExb/vgqNCU=
golang.org/x/exp/typeparams v0.0.0-20230213192124-5e25df0256eb/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/exp/typeparams v0.0.0-20230809150735-7b3493d9a819 h1:qdPQGUfaPzFgEbhQCulaXk+s0ETcox/vQd+f9YCIuxY=
golang.org/x/exp/typeparams v0.0.0-20230809150735-7b3493d9a819/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -370,8 +379,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -403,8 +412,8 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -424,8 +433,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
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=
@ -465,19 +474,19 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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=
@ -531,8 +540,8 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss=
golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -599,8 +608,12 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20230223222841-637eb2293923 h1:znp6mq/drrY+6khTAlJUDNFFcDGV2ENLYKpMq8SyCds=
google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
google.golang.org/genproto v0.0.0-20230807174057-1744710a1577 h1:Tyk/35yqszRCvaragTn5NnkY6IiKk/XvHzEWepo71N0=
google.golang.org/genproto v0.0.0-20230807174057-1744710a1577/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4=
google.golang.org/genproto/googleapis/api v0.0.0-20230807174057-1744710a1577 h1:xv8KoglAClYGkprUSmDTKaILtzfD8XzG9NYVXMprjKo=
google.golang.org/genproto/googleapis/api v0.0.0-20230807174057-1744710a1577/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230807174057-1744710a1577 h1:wukfNtZmZUurLN/atp2hiIeTKn7QJWIQdHzqmsOnAOk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230807174057-1744710a1577/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@ -617,8 +630,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 h1:rNBFJjBCOgVr9pWD7rs/knKL4FRTKgpZmsRfV214zcA=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0/go.mod h1:Dk1tviKTvMCz5tvh7t+fh94dhmQVHuCt2OzJB3CTW9Y=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
@ -633,8 +646,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -32,4 +32,3 @@ actions:
- title: Restart Plex
icon: smile
shell: docker restart plex

View File

@ -17,5 +17,3 @@ describe('Homepage rendering', () => {
})
})
});

View File

@ -10,5 +10,3 @@ describe('Hidden Footer', () => {
})
})
});

View File

@ -14,5 +14,3 @@ describe('Hidden Nav', () => {
})
})
});

View File

@ -23,4 +23,3 @@
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })

View File

@ -8,4 +8,3 @@ cp -f ./configs/config.$1.yaml ./configs/config.yaml
docker start olivetin
NO_COLOR=1 ./node_modules/.bin/cypress run --headless -s cypress/integration/$1/* || true
docker kill olivetin

File diff suppressed because it is too large Load Diff

View File

@ -75,7 +75,10 @@ func getMetdataKeyOrEmpty(md metadata.MD, key string) string {
func UserFromContext(ctx context.Context, cfg *config.Config) *AuthenticatedUser {
md, ok := metadata.FromIncomingContext(ctx)
ret := &AuthenticatedUser{}
ret := &AuthenticatedUser{
Username: "guest",
Usergroup: "guest",
}
if ok {
ret.Username = getMetdataKeyOrEmpty(md, "username")
@ -87,7 +90,7 @@ func UserFromContext(ctx context.Context, cfg *config.Config) *AuthenticatedUser
log.WithFields(log.Fields{
"username": ret.Username,
"usergroup": ret.Usergroup,
}).Infof("UserFromContext")
}).Debugf("UserFromContext")
return ret
}

View File

@ -12,7 +12,9 @@ type Action struct {
Acls []string
ExecOnStartup bool
ExecOnCron []string
MaxConcurrent int
Arguments []ActionArgument
PopupOnStart bool
}
// ActionArgument objects appear on Actions.
@ -73,9 +75,10 @@ type Config struct {
ShowNavigation bool
ShowNewVersions bool
AuthJwtCookieName string
AuthJwtSecret string
AuthJwtSecret string // mutually exclusive with pub key config fields
AuthJwtClaimUsername string
AuthJwtClaimUserGroup string
AuthJwtPubKeyPath string // will read pub key from file on disk
AuthHttpHeaderUsername string
AuthHttpHeaderUserGroup string
DefaultPermissions PermissionsList

View File

@ -30,6 +30,10 @@ func (action *Action) sanitize() {
action.Icon = lookupHTMLIcon(action.Icon)
if action.MaxConcurrent < 1 {
action.MaxConcurrent = 1
}
for idx := range action.Arguments {
action.Arguments[idx].sanitize()
}

View File

@ -22,8 +22,9 @@ var (
func parseActionArguments(rawShellCommand string, values map[string]string, action *config.Action) (string, error) {
log.WithFields(log.Fields{
"actionTitle": action.Title,
"cmd": rawShellCommand,
}).Infof("Before Parse Args")
}).Infof("Action parse args - Before")
r := regexp.MustCompile("{{ *?([a-zA-Z0-9_]+?) *?}}")
matches := r.FindAllStringSubmatch(rawShellCommand, -1)
@ -51,8 +52,9 @@ func parseActionArguments(rawShellCommand string, values map[string]string, acti
}
log.WithFields(log.Fields{
"actionTitle": action.Title,
"cmd": rawShellCommand,
}).Infof("After Parse Args")
}).Infof("Action parse args - After")
return rawShellCommand, nil
}

View File

@ -8,91 +8,77 @@ import (
"bytes"
"context"
"fmt"
"io"
"os/exec"
"runtime"
"time"
)
// Executor represents a helper class for executing commands. It's main method
// is ExecRequest
type Executor struct {
Logs map[string]*InternalLogEntry
listeners []listener
chainOfCommand []executorStepFunc
}
// ExecutionRequest is a request to execute an action. It's passed to an
// Executor. They're created from the grpcapi.
type ExecutionRequest struct {
ActionName string
Arguments map[string]string
UUID string
Tags []string
action *config.Action
Cfg *config.Config
AuthenticatedUser *acl.AuthenticatedUser
logEntry *InternalLogEntry
finalParsedCommand string
executor *Executor
}
// InternalLogEntry objects are created by an Executor, and represent the final
// state of execution (even if the command is not executed). It's designed to be
// easily serializable.
type InternalLogEntry struct {
Datetime string
DatetimeStarted string
DatetimeFinished string
Stdout string
Stderr string
StdoutBuffer io.ReadCloser
StderrBuffer io.ReadCloser
TimedOut bool
Blocked bool
ExitCode int32
Tags []string
ExecutionStarted bool
ExecutionFinished bool
/*
The following two properties are obviously on Action normally, but it's useful
The following 3 properties are obviously on Action normally, but it's useful
that logs are lightweight (so we don't need to have an action associated to
logs, etc. Therefore, we duplicate those values here.
*/
ActionTitle string
ActionIcon string
UUID string
}
type executorStepFunc func(*ExecutionRequest) bool
// Executor represents a helper class for executing commands. It's main method
// is ExecRequest
type Executor struct {
Logs []InternalLogEntry
chainOfCommand []executorStepFunc
}
// ExecRequest processes an ExecutionRequest
func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
req.logEntry = &InternalLogEntry{
Datetime: time.Now().Format("2006-01-02 15:04:05"),
ActionTitle: req.ActionName,
Stdout: "",
Stderr: "",
ExitCode: -1337, // If an Action is not actually executed, this is the default exit code.
}
for _, step := range e.chainOfCommand {
if !step(req) {
break
}
}
e.Logs = append(e.Logs, *req.logEntry)
return &pb.StartActionResponse{
LogEntry: &pb.LogEntry{
ActionTitle: req.logEntry.ActionTitle,
ActionIcon: req.logEntry.ActionIcon,
Datetime: req.logEntry.Datetime,
Stderr: req.logEntry.Stderr,
Stdout: req.logEntry.Stdout,
TimedOut: req.logEntry.TimedOut,
ExitCode: req.logEntry.ExitCode,
},
}
}
// DefaultExecutor returns an Executor, with a sensible "chain of command" for
// executing actions.
func DefaultExecutor() *Executor {
e := Executor{}
e.Logs = make(map[string]*InternalLogEntry)
e.chainOfCommand = []executorStepFunc{
stepLogRequested,
stepFindAction,
stepConcurrencyCheck,
stepACLCheck,
stepParseArgs,
stepLogStart,
@ -103,6 +89,88 @@ func DefaultExecutor() *Executor {
return &e
}
type listener interface {
OnExecutionStarted(actionName string)
OnExecutionFinished(logEntry *InternalLogEntry)
}
func (e *Executor) AddListener(m listener) {
e.listeners = append(e.listeners, m)
}
// ExecRequest processes an ExecutionRequest
func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
// 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.executor = e
req.logEntry = &InternalLogEntry{
DatetimeStarted: time.Now().Format("2006-01-02 15:04:05"),
ActionTitle: req.ActionName,
UUID: req.UUID,
Stdout: "",
Stderr: "",
ExitCode: -1337, // If an Action is not actually executed, this is the default exit code.
ExecutionStarted: false,
ExecutionFinished: false,
}
e.Logs[req.UUID] = req.logEntry
for _, listener := range e.listeners {
listener.OnExecutionStarted(req.ActionName)
}
go e.execChain(req)
return &pb.StartActionResponse{
ExecutionUuid: req.UUID,
}
}
func (e *Executor) execChain(req *ExecutionRequest) {
for _, step := range e.chainOfCommand {
if !step(req) {
break
}
}
req.logEntry.ExecutionFinished = true
// This isn't a step, because we want to notify all listeners, irrespective
// of how many steps were actually executed.
notifyListeners(req)
}
func getConcurrentCount(req *ExecutionRequest) int {
concurrentCount := 0
for _, log := range req.executor.Logs {
if log.ActionTitle == req.ActionName && !log.ExecutionFinished {
concurrentCount += 1
}
}
return concurrentCount
}
func stepConcurrencyCheck(req *ExecutionRequest) bool {
concurrentCount := getConcurrentCount(req)
// Note that the current execution is counted int the logs, so when checking we +1
if concurrentCount >= (req.action.MaxConcurrent + 1) {
msg := fmt.Sprintf("Blocked from executing. This would mean this action is running %d times concurrently, but this action has maxExecutions set to %d.", concurrentCount, req.action.MaxConcurrent)
log.Warnf(msg)
req.logEntry.Stdout = msg
req.logEntry.Blocked = true
return false
}
return true
}
func stepFindAction(req *ExecutionRequest) bool {
actualAction := req.Cfg.FindAction(req.ActionName)
@ -142,9 +210,17 @@ func stepParseArgs(req *ExecutionRequest) bool {
return true
}
func stepLogRequested(req *ExecutionRequest) bool {
log.WithFields(log.Fields{
"actionTitle": req.ActionName,
}).Infof("Action requested")
return true
}
func stepLogStart(req *ExecutionRequest) bool {
log.WithFields(log.Fields{
"title": req.action.Title,
"actionTitle": req.action.Title,
"timeout": req.action.Timeout,
}).Infof("Action starting")
@ -153,7 +229,7 @@ func stepLogStart(req *ExecutionRequest) bool {
func stepLogFinish(req *ExecutionRequest) bool {
log.WithFields(log.Fields{
"title": req.action.Title,
"actionTitle": req.action.Title,
"stdout": req.logEntry.Stdout,
"stderr": req.logEntry.Stderr,
"timedOut": req.logEntry.TimedOut,
@ -163,6 +239,12 @@ func stepLogFinish(req *ExecutionRequest) bool {
return true
}
func notifyListeners(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)
@ -181,8 +263,17 @@ func stepExec(req *ExecutionRequest) bool {
cmd := wrapCommandInShell(ctx, req.finalParsedCommand)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
req.logEntry.StdoutBuffer, _ = cmd.StdoutPipe()
req.logEntry.StderrBuffer, _ = cmd.StderrPipe()
runerr := cmd.Run()
req.logEntry.ExecutionStarted = true
runerr := cmd.Start()
cmd.Wait()
// req.logEntry.Stdout = req.logEntry.StdoutBuffer.String()
// req.logEntry.Stderr = req.logEntry.StderrBuffer.String()
req.logEntry.ExitCode = int32(cmd.ProcessState.ExitCode())
req.logEntry.Stdout = stdout.String()
@ -197,6 +288,7 @@ func stepExec(req *ExecutionRequest) bool {
}
req.logEntry.Tags = req.Tags
req.logEntry.DatetimeFinished = time.Now().Format("2006-01-02 15:04:05")
return true
}

View File

@ -7,6 +7,7 @@ import (
"google.golang.org/grpc"
"net"
"sort"
acl "github.com/OliveTin/OliveTin/internal/acl"
config "github.com/OliveTin/OliveTin/internal/config"
@ -19,7 +20,7 @@ var (
)
type oliveTinAPI struct {
pb.UnimplementedOliveTinApiServer
pb.UnimplementedOliveTinApiServiceServer
executor *executor.Executor
}
@ -35,6 +36,7 @@ func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *pb.StartActionRequest)
execReq := executor.ExecutionRequest{
ActionName: req.ActionName,
UUID: req.Uuid,
Arguments: args,
AuthenticatedUser: acl.UserFromContext(ctx, cfg),
Cfg: cfg,
@ -43,6 +45,62 @@ func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *pb.StartActionRequest)
return api.executor.ExecRequest(&execReq), nil
}
func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *pb.ExecutionStatusRequest) (*pb.ExecutionStatusResponse, error) {
res := &pb.ExecutionStatusResponse{}
logEntry, ok := api.executor.Logs[req.ExecutionUuid]
if !ok {
return res, nil
}
res.LogEntry = &pb.LogEntry{
ActionTitle: logEntry.ActionTitle,
ActionIcon: logEntry.ActionIcon,
DatetimeStarted: logEntry.DatetimeStarted,
DatetimeFinished: logEntry.DatetimeFinished,
Stdout: logEntry.Stdout,
Stderr: logEntry.Stderr,
TimedOut: logEntry.TimedOut,
Blocked: logEntry.Blocked,
ExitCode: logEntry.ExitCode,
Tags: logEntry.Tags,
ExecutionUuid: logEntry.UUID,
ExecutionStarted: logEntry.ExecutionStarted,
ExecutionFinished: logEntry.ExecutionFinished,
}
return res, nil
}
/**
func (api *oliveTinAPI) WatchExecution(req *pb.WatchExecutionRequest, srv pb.OliveTinApi_WatchExecutionServer) error {
log.Infof("Watch")
if logEntry, ok := api.executor.Logs[req.ExecutionUuid]; !ok {
log.Errorf("Execution not found: %v", req.ExecutionUuid)
return nil
} else {
if logEntry.ExecutionStarted {
for !logEntry.ExecutionCompleted {
tmp := make([]byte, 256)
red, err := io.ReadAtLeast(logEntry.StdoutBuffer, tmp, 1)
log.Infof("%v %v", red, err)
srv.Send(&pb.WatchExecutionUpdate{
Update: string(tmp),
})
}
}
return nil
}
}
*/
func (api *oliveTinAPI) GetDashboardComponents(ctx ctx.Context, req *pb.GetDashboardComponentsRequest) (*pb.GetDashboardComponentsResponse, error) {
user := acl.UserFromContext(ctx, cfg)
@ -62,19 +120,30 @@ func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *pb.GetLogsRequest) (*pb.Ge
// TODO Limit to 10 entries or something to prevent browser lag.
for _, logEntry := range api.executor.Logs {
for uuid, logEntry := range api.executor.Logs {
ret.Logs = append(ret.Logs, &pb.LogEntry{
ActionTitle: logEntry.ActionTitle,
ActionIcon: logEntry.ActionIcon,
Datetime: logEntry.Datetime,
DatetimeStarted: logEntry.DatetimeStarted,
DatetimeFinished: logEntry.DatetimeFinished,
Stdout: logEntry.Stdout,
Stderr: logEntry.Stderr,
TimedOut: logEntry.TimedOut,
Blocked: logEntry.Blocked,
ExitCode: logEntry.ExitCode,
Tags: logEntry.Tags,
ExecutionUuid: uuid,
ExecutionStarted: logEntry.ExecutionStarted,
ExecutionFinished: logEntry.ExecutionFinished,
})
}
sorter := func(i, j int) bool {
return ret.Logs[i].DatetimeStarted < ret.Logs[j].DatetimeStarted
}
sort.Slice(ret.Logs, sorter)
return ret, nil
}
@ -130,7 +199,7 @@ func Start(globalConfig *config.Config, ex *executor.Executor) {
}
grpcServer := grpc.NewServer()
pb.RegisterOliveTinApiServer(grpcServer, newServer(ex))
pb.RegisterOliveTinApiServiceServer(grpcServer, newServer(ex))
err = grpcServer.Serve(lis)

View File

@ -29,6 +29,7 @@ func actionCfgToPb(action config.Action, user *acl.AuthenticatedUser) *pb.Action
Title: action.Title,
Icon: action.Icon,
CanExec: acl.IsAllowedExec(cfg, user, &action),
PopupOnStart: action.PopupOnStart,
}
for _, cfgArg := range action.Arguments {

View File

@ -26,7 +26,7 @@ func init() {
lis = bufconn.Listen(bufSize)
s := grpc.NewServer()
pb.RegisterOliveTinApiServer(s, newServer(ex))
pb.RegisterOliveTinApiServiceServer(s, newServer(ex))
go func() {
if err := s.Serve(lis); err != nil {
@ -39,7 +39,7 @@ func bufDialer(context.Context, string) (net.Conn, error) {
return lis.Dial()
}
func getNewTestServerAndClient(t *testing.T, injectedConfig *config.Config) (*grpc.ClientConn, pb.OliveTinApiClient) {
func getNewTestServerAndClient(t *testing.T, injectedConfig *config.Config) (*grpc.ClientConn, pb.OliveTinApiServiceClient) {
cfg = injectedConfig
ctx := context.Background()
@ -50,7 +50,7 @@ func getNewTestServerAndClient(t *testing.T, injectedConfig *config.Config) (*gr
t.Fatalf("Failed to dial bufnet: %v", err)
}
client := pb.NewOliveTinApiClient(conn)
client := pb.NewOliveTinApiServiceClient(conn)
return conn, client
}

View File

@ -59,6 +59,10 @@ func parseRequestMetadata(ctx context.Context, req *http.Request) metadata.MD {
return md
}
func SetGlobalRestConfig(config *config.Config) {
cfg = config
}
func startRestAPIServer(globalConfig *config.Config) error {
cfg = globalConfig
@ -76,7 +80,7 @@ func startRestAPIServer(globalConfig *config.Config) error {
runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.HTTPBodyMarshaler{
Marshaler: &runtime.JSONPb{
MarshalOptions: protojson.MarshalOptions{
UseProtoNames: true,
UseProtoNames: false, // eg: canExec for js instead of can_exec from protobuf
EmitUnpopulated: true,
},
},
@ -84,7 +88,7 @@ func startRestAPIServer(globalConfig *config.Config) error {
)
opts := []grpc.DialOption{grpc.WithInsecure()}
err := gw.RegisterOliveTinApiHandlerFromEndpoint(ctx, mux, cfg.ListenAddressGrpcActions, opts)
err := gw.RegisterOliveTinApiServiceHandlerFromEndpoint(ctx, mux, cfg.ListenAddressGrpcActions, opts)
if err != nil {
log.Errorf("Could not register REST API Handler %v", err)

View File

@ -1,14 +1,58 @@
package httpservers
import (
"crypto/rsa"
"errors"
"fmt"
"github.com/golang-jwt/jwt/v4"
log "github.com/sirupsen/logrus"
"net/http"
"os"
)
func parseJwtToken(cookieValue string) (*jwt.Token, error) {
var (
pubKeyBytes []byte = nil
pubKey *rsa.PublicKey
)
func readPublicKey() error {
if pubKeyBytes != nil {
return nil // Already read.
}
pubKeyBytes, err := os.ReadFile(cfg.AuthJwtPubKeyPath)
if err != nil {
return fmt.Errorf("couldn't read public key from file %s", cfg.AuthJwtPubKeyPath)
}
// Since the token is RSA (which we validated at the start of this function), the return type of this function actually has to be rsa.PublicKey!
pubKey, err = jwt.ParseRSAPublicKeyFromPEM(pubKeyBytes)
if err != nil {
return fmt.Errorf("error parsing public key object (from %s)", cfg.AuthJwtPubKeyPath)
}
return nil
}
func parseJwtTokenWithKey(cookieValue string) (*jwt.Token, error) {
err := readPublicKey()
if err != nil {
return nil, err
}
return jwt.Parse(cookieValue, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf(
"expected token algorithm '%v' but got '%v'",
jwt.SigningMethodRS256.Name,
token.Header)
}
return pubKey, nil
})
}
func parseJwtTokenWithoutKey(cookieValue string) (*jwt.Token, error) {
return jwt.Parse(cookieValue, func(token *jwt.Token) (interface{}, error) {
// Don't forget to validate the alg is what you expect:
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
@ -20,6 +64,14 @@ func parseJwtToken(cookieValue string) (*jwt.Token, error) {
})
}
func parseJwtToken(cookieValue string) (*jwt.Token, error) {
if cfg.AuthJwtPubKeyPath != "" { // activate this path only if pub key is specified
return parseJwtTokenWithKey(cookieValue)
} else {
return parseJwtTokenWithoutKey(cookieValue)
}
}
func getClaimsFromJwtToken(cookieValue string) (jwt.MapClaims, error) {
token, err := parseJwtToken(cookieValue)

View File

@ -0,0 +1,125 @@
package httpservers
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
config2 "github.com/OliveTin/OliveTin/internal/config"
"github.com/OliveTin/OliveTin/internal/cors"
"github.com/golang-jwt/jwt/v4"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/encoding/protojson"
"io"
"net"
"net/http"
"os"
"testing"
"time"
)
func createKeys() (*rsa.PrivateKey, string) {
tmpFile, _ := os.CreateTemp(os.TempDir(), "olivetin-jwt-")
defer os.Remove(tmpFile.Name())
fmt.Println("Created File: " + tmpFile.Name())
privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
pubKey := &privateKey.PublicKey
// https://stackoverflow.com/questions/13555085/save-and-load-crypto-rsa-privatekey-to-and-from-the-disk
pkixPubKey, _ := x509.MarshalPKIXPublicKey(pubKey)
pubPem := pem.EncodeToMemory(
&pem.Block{
Type: "RSA PUBLIC KEY",
Bytes: pkixPubKey,
},
)
if err := os.WriteFile(tmpFile.Name(), pubPem, 0755); err != nil {
fmt.Printf("error when dumping pubKey: %s \n", err)
}
return privateKey, tmpFile.Name()
}
func testBase(t *testing.T, expire int64, expectCode int) {
privateKey, publicKeyPath := createKeys()
// default config + overrides
config := config2.DefaultConfig()
config.AuthJwtPubKeyPath = publicKeyPath
config.AuthJwtClaimUsername = "sub"
config.AuthJwtClaimUserGroup = "olivetinGroup"
config.AuthJwtCookieName = "authorization_token"
SetGlobalRestConfig(config) // ugly, setting global var, we should pass configs as params to modules... :/
token := jwt.New(jwt.SigningMethodRS256)
claims := token.Claims.(jwt.MapClaims)
claims["nbf"] = time.Now().Unix() - 1000
claims["exp"] = time.Now().Unix() + expire
claims["sub"] = "test"
claims["olivetinGroup"] = "test"
tokenStr, _ := token.SignedString(privateKey)
// init mux endpoint like in restapi.go (but using dummy response handler)
mux := runtime.NewServeMux(
runtime.WithMetadata(parseRequestMetadata), // i am guessing this is critical middleware for authorizing request cookie
runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.HTTPBodyMarshaler{
Marshaler: &runtime.JSONPb{
MarshalOptions: protojson.MarshalOptions{
UseProtoNames: true,
EmitUnpopulated: true,
},
},
}),
)
mux.HandlePath("GET", "/", func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
username, usergroup := parseJwtCookie(r)
if username == "" {
w.WriteHeader(403)
}
w.Write([]byte(fmt.Sprintf("username=%v, usergroup=%v", username, usergroup)))
})
// make server and attach handler
srv := &http.Server{Handler: cors.AllowCors(mux)}
lis, _ := net.Listen("tcp", ":1337")
go func() {
if err := srv.Serve(lis); err != nil {
t.Errorf("couldn't start server: %v", err)
}
}()
// make http client and send request to myself
client := &http.Client{}
req, _ := http.NewRequest("GET", "http://localhost:1337/", nil)
cookie := &http.Cookie{
Name: "authorization_token",
Value: tokenStr,
MaxAge: 300,
}
req.AddCookie(cookie)
res, err := client.Do(req)
if err != nil {
assert.Equal(t, expectCode, -1)
} else {
defer res.Body.Close()
assert.Equal(t, expectCode, res.StatusCode)
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}
}
func TestJWTSignatureVerificationSucceeds(t *testing.T) {
testBase(t, 1000, 200)
}
func TestJWTSignatureVerificationFails(t *testing.T) {
testBase(t, -500, 403)
}

View File

@ -10,10 +10,12 @@ away, and several other issues.
import (
config "github.com/OliveTin/OliveTin/internal/config"
"github.com/OliveTin/OliveTin/internal/websocket"
log "github.com/sirupsen/logrus"
"net/http"
"net/http/httputil"
"net/url"
"strings"
)
// StartSingleHTTPFrontend will create a reverse proxy that proxies the API
@ -37,8 +39,12 @@ func StartSingleHTTPFrontend(cfg *config.Config) {
})
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.Header.Get("Connection"), "Upgrade") {
websocket.HandleWebsocket(w, r)
} else {
log.Debugf("ui req: %q", r.URL)
webuiProxy.ServeHTTP(w, r)
}
})
srv := &http.Server{

View File

@ -1,6 +1,7 @@
package httpservers
import (
config "github.com/OliveTin/OliveTin/internal/config"
"github.com/stretchr/testify/assert"
"os"
"testing"
@ -9,6 +10,8 @@ import (
func TestGetWebuiDir(t *testing.T) {
os.Chdir("../../") // go test sets the cwd to "httpservers" by default
cfg = config.DefaultConfig()
dir := findWebuiDir()
assert.Equal(t, "./webui", dir, "Finding the webui dir")

View File

@ -0,0 +1,122 @@
package websocket
import (
pb "github.com/OliveTin/OliveTin/gen/grpc"
"github.com/OliveTin/OliveTin/internal/executor"
ws "github.com/gorilla/websocket"
log "github.com/sirupsen/logrus"
"google.golang.org/protobuf/encoding/protojson"
"net/http"
)
var upgrader = ws.Upgrader{}
type WebsocketClient struct {
conn *ws.Conn
}
var clients []*WebsocketClient
var marshalOptions = protojson.MarshalOptions{
UseProtoNames: false, // eg: canExec for js instead of can_exec from protobuf
EmitUnpopulated: true,
}
var ExecutionListener WebsocketExecutionListener
type WebsocketExecutionListener struct{}
func (WebsocketExecutionListener) OnExecutionStarted(title string) {
/*
broadcast(ExecutionStarted{
Type: "ExecutionStarted",
Action: title,
});
*/
}
func (WebsocketExecutionListener) OnExecutionFinished(logEntry *executor.InternalLogEntry) {
le := &pb.LogEntry{
ActionTitle: logEntry.ActionTitle,
ActionIcon: logEntry.ActionIcon,
DatetimeStarted: logEntry.DatetimeStarted,
DatetimeFinished: logEntry.DatetimeFinished,
Stdout: logEntry.Stdout,
Stderr: logEntry.Stderr,
TimedOut: logEntry.TimedOut,
Blocked: logEntry.Blocked,
ExitCode: logEntry.ExitCode,
Tags: logEntry.Tags,
Uuid: logEntry.UUID,
ExecutionStarted: logEntry.ExecutionStarted,
ExecutionFinished: logEntry.ExecutionFinished,
}
broadcast("ExecutionFinished", le)
}
func broadcast(messageType string, pbmsg *pb.LogEntry) {
payload, err := marshalOptions.Marshal(pbmsg)
// <EVIL>
// So, the websocket wants to encode messages using the same protomarshaller
// as the REST API - this gives consistency instead of using encoding/json
// and allows us to set specific marshalOptions.
//
// However, the protomarshaller will marshal the type, but the JavaScript at
// the other end has no idea what type this object is - as we're just sending
// it as JSON over the websocket.
//
// Therefore, we wrap the nicely marsheled bytes in a hacky JSON string
// literal and encode that string just with a byte array cast.
hackyMessageEnvelope := "{\"type\": \"" + messageType + "\", \"payload\": "
hackyMessage := []byte{}
hackyMessage = append(hackyMessage, []byte(hackyMessageEnvelope)...)
hackyMessage = append(hackyMessage, payload...)
hackyMessage = append(hackyMessage, []byte("}")...)
// </EVIL>
if err != nil {
log.Errorf("websocket marshal error: %v", err)
return
}
for _, client := range clients {
client.conn.WriteMessage(1, hackyMessage)
}
}
func (c *WebsocketClient) messageLoop() {
for {
mt, message, err := c.conn.ReadMessage()
if err != nil {
log.Printf("err: %v", err)
break
}
log.Infof("websocket recv: %s %d", message, mt)
}
}
func HandleWebsocket(w http.ResponseWriter, r *http.Request) bool {
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Warnf("Websocket issue: %v", err)
return false
}
// defer c.Close()
wsclient := &WebsocketClient{
conn: c,
}
clients = append(clients, wsclient)
go wsclient.messageLoop()
return true
}

View File

@ -62,19 +62,62 @@
</main>
<footer title = "footer">
<p><img title = "application icon" src = "OliveTinLogo.png" height = "1em" class = "logo" /> OliveTin</p>
<p><img title = "application icon" src = "OliveTinLogo.png" alt = "OliveTin logo" height = "1em" class = "logo" /> OliveTin</p>
<p>
<a href = "https://docs.olivetin.app" target = "_new">Documentation</a> |
<a href = "https://github.com/OliveTin/OliveTin/issues/new/choose" target = "_new">Raise an issue on GitHub</a> |
<span id = "currentVersion">Version: ?</p>
<span>Version: <span id = "currentVersion">?</span></span> |
<span>Server connection: <span id = "serverConnection">?</span></span>
</p>
<p>
<a id = "available-version" href = "http://olivetin.app" target = "_blank" hidden>?</a>
</p>
</footer>
<dialog id = "executionResults">
<div class = "action-header">
<span class = "icon" role = "icon"></span>
<h2>Log:
<span class = "title">?</span>
</h2>
</div>
<p>
<strong>Started: </strong><span class = "datetimeStarted">unknown</span>.
<strong>Finished: </strong><span class = "datetimeFinished">unknown</span>
</p>
<p>
<strong>Exit Code: </strong><span class = "exitCode">unknown</span>
</p>
<p>
<strong>Status: </strong><span class = "status">unknown</span>
</p>
<details>
<summary>stdout</summary>
<pre class = "stdout">
?
</pre>
</details>
<details>
<summary>stderr</summary>
<pre class = "stderr">
?
</pre>
</details>
<form method = "dialog">
<button name = "cancel">Close</button>
</form>
</dialog>
<template id = "tplArgumentForm">
<dialog>
<form class = "action-arguments">
<div class = "wrapper">
<div>
<div class = "action-header">
<span class = "icon" role = "icon"></span>
<h2>Argument form</h2>
</div>
@ -86,14 +129,20 @@
<button name = "cancel">Cancel</button>
</div>
</div>
<form>
</form>
</button>
</template>
<template id = "tplActionButton">
<div>
<button>
<span role = "icon" title = "button icon" class = "icon">&#x1f4a9;</span>
<p role = "title" class = "title">Untitled Button</p>
<span role = "title" class = "title">Untitled Button</span>
</button>
<div class = "executions">
</div>
</div>
</template>
<template id = "tplLogRow">
@ -129,7 +178,10 @@
to at least display a helpful error message if we can't use OliveTin.
*/
function showBigError (type, friendlyType, message) {
clearInterval(window.buttonInterval)
for (const oldError of document.querySelectorAll('div.error').values()) {
window.old = oldError;
oldError.remove();
}
console.error('Error ' + type + ': ', message)
@ -146,5 +198,6 @@
</script>
<script type = "module" src = "main.js"></script>
<script type = "module" src = "js/websocket.js"></script>
</body>
</html>

View File

@ -1,5 +1,5 @@
import { marshalLogsJsonToHtml } from './marshaller.js'
import './ArgumentForm.js'
import './ExecutionButton.js'
class ActionButton extends window.HTMLElement {
constructFromJson (json) {
@ -25,6 +25,7 @@ class ActionButton extends window.HTMLElement {
})
document.body.appendChild(frm)
frm.querySelector('dialog').showModal()
} else {
this.startAction()
}
@ -32,7 +33,8 @@ class ActionButton extends window.HTMLElement {
this.updateFromJson(json)
this.updateDom()
this.domTitle.innerText = this.btn.title
this.domIcon.innerHTML = this.unicodeIcon
this.setAttribute('id', 'actionButton_' + json.id)
}
@ -52,20 +54,27 @@ class ActionButton extends window.HTMLElement {
}
startAction (actionArgs) {
this.btn.disabled = true
this.isWaiting = true
this.updateDom()
// this.btn.disabled = true
// this.isWaiting = true
// this.updateDom()
this.btn.classList = [] // Removes old animation classes
if (actionArgs === undefined) {
actionArgs = []
}
// UUIDs are create client side, so that we can setup a "execution-button"
// to track the execution before we send the request to the server.
const startActionArgs = {
actionName: this.btn.title,
arguments: actionArgs
arguments: actionArgs,
uuid: window.crypto.randomUUID()
}
const btnExecution = document.createElement('execution-button')
btnExecution.constructFromJson(startActionArgs.uuid)
this.querySelector('div.executions').appendChild(btnExecution)
window.fetch(this.actionCallUrl, {
method: 'POST',
headers: {
@ -80,45 +89,12 @@ class ActionButton extends window.HTMLElement {
}
}
).then((json) => {
marshalLogsJsonToHtml({ logs: [json.logEntry] })
if (json.logEntry.timedOut) {
this.onActionResult('action-timeout', 'Timed out')
} else if (json.logEntry.exitCode === -1337) {
this.onActionError('Error')
} else if (json.logEntry.exitCode !== 0) {
this.onActionResult('action-nonzero-exit', 'Exit code ' + json.logEntry.exitCode)
} else {
this.onActionResult('action-success', 'Success!')
}
// The button used to wait for the action to finish, but now it is fire & forget
}).catch(err => {
this.onActionError(err)
})
}
onActionResult (cssClass, temporaryStatusMessage) {
this.btn.disabled = false
this.temporaryStatusMessage = '[ ' + temporaryStatusMessage + ' ]'
this.updateDom()
this.btn.classList.add(cssClass)
setTimeout(() => {
this.btn.classList.remove(cssClass)
}, 1000)
}
onActionError (err) {
console.error('callback error', err)
this.btn.disabled = false
this.isWaiting = false
this.updateDom()
this.btn.classList.add('action-failed')
setTimeout(() => {
this.btn.classList.remove('action-failed')
}, 1000)
}
constructDomFromTemplate () {
const tpl = document.getElementById('tplActionButton')
const content = tpl.content.cloneNode(true)
@ -134,27 +110,6 @@ class ActionButton extends window.HTMLElement {
this.domTitle = this.btn.querySelector('.title')
this.domIcon = this.btn.querySelector('.icon')
}
updateDom () {
if (this.temporaryStatusMessage != null) {
this.domTitle.innerText = this.temporaryStatusMessage
this.domTitle.classList.add('temporary-status-message')
this.isWaiting = false
this.disabled = false
setTimeout(() => {
this.temporaryStatusMessage = null
this.domTitle.classList.remove('temporary-status-message')
this.updateDom()
}, 2000)
} else if (this.isWaiting) {
this.domTitle.innerText = 'Waiting...'
} else {
this.domTitle.innerText = this.btn.title
}
this.domIcon.innerHTML = this.unicodeIcon
}
}
window.customElements.define('action-button', ActionButton)

105
webui/js/ExecutionButton.js Normal file
View File

@ -0,0 +1,105 @@
import { ExecutionDialog } from './ExecutionDialog.js'
class ExecutionButton extends window.HTMLElement {
constructFromJson (json) {
this.executionUuid = json
this.appendChild(document.createElement('button'))
this.isWaiting = true
this.setAttribute('id', 'execution-' + json)
this.btn = this.querySelector('button')
this.btn.innerText = 'Executing...'
this.btn.onclick = () => {
this.show()
}
}
show () {
if (window.executionDialog === undefined) {
window.executionDialog = new ExecutionDialog()
}
const executionStatusArgs = {
executionUuid: this.executionUuid
}
window.executionDialog.constructFromJson(this.executionUuid)
window.executionDialog.show()
window.fetch(window.restBaseUrl + 'ExecutionStatus', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(executionStatusArgs)
}).then((res) => {
if (res.ok) {
return res.json()
} else {
throw new Error(res.statusText)
}
}
).then((json) => {
window.executionDialog.renderResult(json)
}).catch(err => {
window.executionDialog.renderError(err)
})
}
onFinished (LogEntry) {
if (LogEntry.timedOut) {
this.onActionResult('action-timeout', 'Timed out')
} else if (LogEntry.blocked) {
this.onActionResult('action-blocked', 'Blocked!')
} else if (LogEntry.exitCode !== 0) {
this.onActionResult('action-nonzero-exit', 'Exit code ' + LogEntry.exitCode)
} else {
this.onActionResult('action-success', 'Success!')
}
}
onActionResult (cssClass, temporaryStatusMessage) {
this.temporaryStatusMessage = '[ ' + temporaryStatusMessage + ' ]'
this.updateDom()
this.btn.classList.add(cssClass)
setTimeout(() => {
this.btn.classList.remove(cssClass)
}, 1000)
}
onActionError (err) {
console.error('callback error', err)
this.btn.disabled = false
this.isWaiting = false
this.updateDom()
this.btn.classList.add('action-failed')
setTimeout(() => {
this.btn.classList.remove('action-failed')
}, 1000)
}
updateDom () {
if (this.temporaryStatusMessage != null) {
this.btn.innerText = this.temporaryStatusMessage
this.btn.classList.add('temporary-status-message')
this.isWaiting = false
this.disabled = false
setTimeout(() => {
this.temporaryStatusMessage = null
this.btn.classList.remove('temporary-status-message')
this.updateDom()
}, 2000)
} else if (this.isWaiting) {
this.btn.innerText = 'Waiting...'
} else {
this.btn.innerText = 'Finished'
}
}
}
window.customElements.define('execution-button', ExecutionButton)

View File

@ -0,0 +1,66 @@
// This ExecutionDialog is NOT a custom HTML element, but rather just picks up
// the <dialog /> element out of index.html and just re-uses that - as only
// one dialog can be shown at a time.
export class ExecutionDialog {
constructFromJson (json) {
this.executionUuid = json
this.dlg = document.querySelector('dialog#executionResults')
this.domIcon = this.dlg.querySelector('.icon')
this.domTitle = this.dlg.querySelector('.title')
this.domStdout = this.dlg.querySelector('.stdout')
this.domStderr = this.dlg.querySelector('.stderr')
this.domDatetimeStarted = this.dlg.querySelector('.datetimeStarted')
this.domDatetimeFinished = this.dlg.querySelector('.datetimeFinished')
this.domExitCode = this.dlg.querySelector('.exitCode')
this.domStatus = this.dlg.querySelector('.status')
}
show () {
this.dlg.showModal()
}
renderResult (res) {
this.executionUuid = res.logEntry.executionUuid
if (res.logEntry.executionFinished) {
this.domStatus.innerText = 'Completed'
this.domDatetimeFinished.innerText = res.logEntry.datetimeFinished
if (res.logEntry.blocked) {
this.domStatus.innerText = 'Blocked'
}
if (res.logEntry.timedOut) {
this.domExitCode.innerText = 'Timed out'
this.domStatus.innerText = 'Timed out'
} else {
this.domExitCode.innerText = res.logEntry.exitCode
}
} else {
this.domDatetimeFinished.innerText = 'Still running...'
this.domExitCode.innerText = 'Still running...'
this.domStatus.innerText = 'Still running...'
}
this.domIcon.innerHTML = res.logEntry.actionIcon
this.domTitle.innerText = res.logEntry.actionTitle
this.domStdout.innerText = res.logEntry.stdout
if (res.logEntry.stderr === '') {
this.domStderr.parentElement.hidden = true
this.domStderr.innerText = res.logEntry.stderr
} else {
this.domStderr.parentElement.hidden = false
this.domStderr.innerText = res.logEntry.stderr
}
this.domDatetimeStarted.innerText = res.logEntry.datetimeStarted
}
renderError (err) {
this.dlg.querySelector('pre').innerText = JSON.stringify(err)
}
}

View File

@ -50,7 +50,7 @@ export function marshalLogsJsonToHtml (json) {
logTableExitCode += ' (timed out)'
}
row.querySelector('.timestamp').innerText = logEntry.datetime
row.querySelector('.timestamp').innerText = logEntry.datetimeStarted
row.querySelector('.content').innerText = logEntry.actionTitle
row.querySelector('.icon').innerHTML = logEntry.actionIcon
row.querySelector('pre.stdout').innerText = logEntry.stdout

73
webui/js/websocket.js Normal file
View File

@ -0,0 +1,73 @@
import { marshalLogsJsonToHtml } from './marshaller.js'
window.ws = null
export function checkWebsocketConnection () {
if (window.ws === null || window.ws.readyState === 3) {
reconnectWebsocket()
}
}
function reconnectWebsocket () {
window.websocketAvailable = false
let proto = 'ws:'
if (window.location.protocol === 'https:') {
proto = 'wss:'
}
const websocketConnectionUrl = proto + window.location.host + '/websocket'
const ws = window.ws = new WebSocket(websocketConnectionUrl)
ws.addEventListener('open', websocketOnOpen)
ws.addEventListener('message', websocketOnMessage)
ws.addEventListener('error', websocketOnError)
ws.addEventListener('close', websocketOnClose)
}
function websocketOnOpen (evt) {
window.websocketAvailable = true
window.ws.send('monitor')
window.refreshLoop()
}
function websocketOnMessage (msg) {
// FIXME check msg status is OK
const j = JSON.parse(msg.data)
switch (j.type) {
case 'ExecutionFinished':
updatePageAfterFinished(j.payload)
break
default:
window.showBigError('Unknown message type from server: ' + j.type)
}
}
function updatePageAfterFinished (logEntry) {
document.querySelector('execution-button#execution-' + logEntry.uuid).onFinished(logEntry)
marshalLogsJsonToHtml({
logs: [logEntry]
})
// If the current execution dialog is open, update that too
if (window.executionDialog != null && window.executionDialog.dlg.open && window.executionDialog.executionUuid === logEntry.uuid) {
window.executionDialog.renderResult({
logEntry: logEntry
})
}
}
function websocketOnError (err) {
window.websocketAvailable = false
window.refreshLoop()
console.error(err)
}
function websocketOnClose () {
window.websocketAvailable = false
}

View File

@ -1,6 +1,7 @@
'use strict'
import { marshalActionButtonsJsonToHtml, marshalLogsJsonToHtml } from './js/marshaller.js'
import { checkWebsocketConnection } from './js/websocket.js'
function showSection (name) {
for (const otherName of ['Actions', 'Logs']) {
@ -19,15 +20,42 @@ function setupSections () {
showSection('Actions')
}
function refreshLoop () {
if (window.websocketAvailable) {
document.querySelector('#serverConnection').classList.remove('error')
document.querySelector('#serverConnection').innerText = 'websocket'
// Websocket updates are streamed live, not updated on a loop.
} else if (window.restAvailable) {
// Fallback to rest, but try to reconnect the websocket anyway.
fetchGetDashboardComponents()
checkWebsocketConnection()
document.querySelector('#serverConnection').classList.remove('error')
document.querySelector('#serverConnection').innerText = 'rest'
fetchGetLogs()
} else {
document.querySelector('#serverConnection').innerText = 'disconnected, trying to reconnect...'
document.querySelector('#serverConnection').classList.add('error')
// Still try to fetch the dashboard, if successfull window.restAvailable = true
fetchGetDashboardComponents()
}
}
function fetchGetDashboardComponents () {
window.fetch(window.restBaseUrl + 'GetDashboardComponents', {
cors: 'cors'
}).then(res => {
return res.json()
}).then(res => {
window.restAvailable = true
marshalActionButtonsJsonToHtml(res)
}).catch(err => {
window.showBigError('fetch-buttons', 'getting buttons', err, 'blat')
}).catch(() => { // err is 1st arg
window.restAvailable = false
// window.showBigError('fetch-buttons', 'getting buttons', err, 'blat')
})
}
@ -55,7 +83,7 @@ function processWebuiSettingsJson (settings) {
document.head.appendChild(themeCss)
}
document.querySelector('#currentVersion').innerText = 'Version: ' + settings.CurrentVersion
document.querySelector('#currentVersion').innerText = settings.CurrentVersion
if (settings.ShowNewVersions && settings.AvailableVersion !== 'none') {
document.querySelector('#available-version').innerText = 'New Version Available: ' + settings.AvailableVersion
@ -80,10 +108,11 @@ function main () {
}).then(res => {
processWebuiSettingsJson(res)
fetchGetDashboardComponents()
fetchGetLogs()
window.restAvailable = true
window.refreshLoop = refreshLoop
window.refreshLoop()
window.buttonInterval = setInterval(fetchGetDashboardComponents, 3000)
setInterval(refreshLoop, 3000)
}).catch(err => {
window.showBigError('fetch-webui-settings', 'getting webui settings', err)
})

View File

@ -1053,9 +1053,9 @@
}
},
"node_modules/eslint-plugin-node/node_modules/semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
@ -2639,9 +2639,9 @@
}
},
"node_modules/read-pkg/node_modules/semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true,
"bin": {
"semver": "bin/semver"
@ -2796,9 +2796,9 @@
}
},
"node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
@ -4155,9 +4155,9 @@
"dev": true
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true
}
}
@ -5281,9 +5281,9 @@
}
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true
},
"type-fest": {
@ -5399,9 +5399,9 @@
}
},
"semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"

View File

@ -7,6 +7,13 @@ body {
margin: 0;
}
dialog {
min-width: 600px;
text-align: left;
padding: 1em;
box-shadow: 0 0 6px 0 #444;
}
fieldset {
padding: 0;
}
@ -120,7 +127,7 @@ span[role="icon"] {
vertical-align: middle;
}
form span[role="icon"],
.action-header span[role="icon"],
tr.log-row span[role="icon"] {
display: inline-block;
padding-right: 0.2em;
@ -128,6 +135,9 @@ tr.log-row span[role="icon"] {
.error {
background-color: salmon;
}
div.error {
padding: 1em;
}
@ -146,6 +156,7 @@ div.entity {
}
h2 {
margin-top: 0;
font-size: 1em;
display: inline-block;
}
@ -185,6 +196,11 @@ input[type="submit"]:disabled {
cursor: not-allowed;
}
div.executions button {
margin-top: .2em;
margin-bottom: .2em;
}
fieldset#section-switcher {
border: 0;
text-align: right;
@ -254,6 +270,14 @@ button.active-section {
20% { background-color: cyan; }
}
.action-blocked {
animation: kf-action-blocked 1s;
}
@keyframes kf-action-blocked {
20% { background-color: purple; }
}
footer,
footer a {
color: black;
@ -282,21 +306,9 @@ details[open] {
display: block;
}
form.action-arguments {
position: absolute;
inset: 0;
padding: 1em;
box-shadow: 0 0 6px 0 #aaa;
background-color: #dee3e7;
}
form div.wrapper {
border-radius: 1em;
box-shadow: 0 0 10px 0 #444;
background-color: white;
border: 1px solid #999;
text-align: left;
padding: 1em;
}
label {