feature: websocket implemenation at last!

This commit is contained in:
jamesread 2023-08-25 15:11:29 +01:00
parent 6116e954ba
commit e5a870ed94
21 changed files with 613 additions and 253 deletions

View File

@ -8,16 +8,16 @@ message Action {
string id = 1; string id = 1;
string title = 2; string title = 2;
string icon = 3; string icon = 3;
bool canExec = 4; bool can_exec = 4;
repeated ActionArgument arguments = 5; repeated ActionArgument arguments = 5;
bool popupOnStart = 6; bool popup_on_start = 6;
} }
message ActionArgument { message ActionArgument {
string name = 1; string name = 1;
string title = 2; string title = 2;
string type = 3; string type = 3;
string defaultValue = 4; string default_value = 4;
repeated ActionArgumentChoice choices = 5; repeated ActionArgumentChoice choices = 5;
@ -44,9 +44,11 @@ message GetDashboardComponentsResponse {
message GetDashboardComponentsRequest {} message GetDashboardComponentsRequest {}
message StartActionRequest { message StartActionRequest {
string actionName = 1; string action_name = 1;
repeated StartActionArgument arguments = 2; repeated StartActionArgument arguments = 2;
string uuid = 3;
} }
message StartActionArgument { message StartActionArgument {
@ -55,24 +57,25 @@ message StartActionArgument {
} }
message StartActionResponse { message StartActionResponse {
string executionUuid = 2; string execution_uuid = 2;
} }
message GetLogsRequest{}; message GetLogsRequest{};
message LogEntry { message LogEntry {
string datetimeStarted = 1; string datetime_started = 1;
string actionTitle = 2; string action_title = 2;
string stdout = 3; string stdout = 3;
string stderr = 4; string stderr = 4;
bool timedOut = 5; bool timed_out = 5;
int32 exitCode = 6; int32 exit_code = 6;
string user = 7; string user = 7;
string userClass = 8; string user_class = 8;
string actionIcon = 9; string action_icon = 9;
repeated string tags = 10; repeated string tags = 10;
string executionUuid = 11; string execution_uuid = 11;
string datetimeFinished = 12; string datetime_finished = 12;
string uuid = 13;
} }
message GetLogsResponse { message GetLogsResponse {
@ -90,17 +93,25 @@ message ValidateArgumentTypeResponse {
} }
message WatchExecutionRequest { message WatchExecutionRequest {
string executionUuid = 1; string execution_uuid = 1;
} }
message WatchExecutionUpdate { message WatchExecutionUpdate {
string update = 1; string update = 1;
} }
message ExecutionStatusRequest {
string execution_uuid = 1;
}
message ExecutionStatusResponse {
LogEntry log_entry = 1;
}
message WhoAmIRequest {} message WhoAmIRequest {}
message WhoAmIResponse { message WhoAmIResponse {
string authenticatedUser = 1; string authenticated_user = 1;
} }
message SosReportRequest {} message SosReportRequest {}
@ -109,7 +120,7 @@ message SosReportResponse {
string alert = 1; string alert = 1;
} }
service OliveTinApi { service OliveTinApiService {
rpc GetDashboardComponents(GetDashboardComponentsRequest) returns (GetDashboardComponentsResponse) { rpc GetDashboardComponents(GetDashboardComponentsRequest) returns (GetDashboardComponentsResponse) {
option (google.api.http) = { option (google.api.http) = {
get: "/api/GetDashboardComponents" get: "/api/GetDashboardComponents"
@ -123,9 +134,9 @@ service OliveTinApi {
}; };
} }
rpc WatchExecution(WatchExecutionRequest) returns (stream WatchExecutionUpdate) { rpc ExecutionStatus(ExecutionStatusRequest) returns (ExecutionStatusResponse) {
option (google.api.http) = { option (google.api.http) = {
post: "/api/WatchExecution" post: "/api/ExecutionStatus"
body: "*" body: "*"
}; };
} }

View File

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

2
go.mod
View File

@ -9,6 +9,7 @@ require (
github.com/go-critic/go-critic v0.8.2 github.com/go-critic/go-critic v0.8.2
github.com/golang-jwt/jwt/v4 v4.5.0 github.com/golang-jwt/jwt/v4 v4.5.0
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/gorilla/websocket v1.4.1
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.2 github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.2
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
@ -20,7 +21,6 @@ require (
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0
google.golang.org/protobuf v1.31.0 google.golang.org/protobuf v1.31.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
nhooyr.io/websocket v1.8.7
) )
require ( require (

39
go.sum
View File

@ -94,10 +94,6 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 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 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= 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-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 h1:mekhZ9jw5NBEj3I8o/EywXw5zBfGAJuMo4VVVjtxF80=
@ -110,13 +106,6 @@ 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/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 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8= github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8=
github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU= github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU=
github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s= github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s=
@ -135,12 +124,6 @@ github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQi
github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ= github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ=
github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus= github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus=
github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig= github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M= github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
@ -194,7 +177,6 @@ github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-containerregistry v0.16.1 h1:rUEt426sR6nyrL3gt+18ibRcvYpKYdpsa5ZW7MA08dQ= 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/go-containerregistry v0.16.1/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 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.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@ -234,13 +216,10 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84 h1:2uT3aivO7NVpUPGcQX7RbHijHMyWix/yCnIrCWc+5co= github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84 h1:2uT3aivO7NVpUPGcQX7RbHijHMyWix/yCnIrCWc+5co=
github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84/go.mod h1:Zi/ZFkEqFHTm7qkjyNJjaWH4LQA9LQhGJyF0lTYGpxw= github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84/go.mod h1:Zi/ZFkEqFHTm7qkjyNJjaWH4LQA9LQhGJyF0lTYGpxw=
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/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/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= 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/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 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
@ -251,22 +230,14 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/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 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
@ -320,7 +291,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@ -333,10 +303,6 @@ 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/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/tetratelabs/wazero v1.4.0 h1:9/MirYvmkJ/zSUOygKY/ia3t+e+RqIZXKbylIby1WYk= 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/tetratelabs/wazero v1.4.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts= 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/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.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -482,7 +448,6 @@ golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -691,8 +656,6 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@ -704,8 +667,6 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@ -90,7 +90,7 @@ func UserFromContext(ctx context.Context, cfg *config.Config) *AuthenticatedUser
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"username": ret.Username, "username": ret.Username,
"usergroup": ret.Usergroup, "usergroup": ret.Usergroup,
}).Infof("UserFromContext") }).Debugf("UserFromContext")
return ret return ret
} }

View File

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

View File

@ -4,7 +4,6 @@ import (
pb "github.com/OliveTin/OliveTin/gen/grpc" pb "github.com/OliveTin/OliveTin/gen/grpc"
acl "github.com/OliveTin/OliveTin/internal/acl" acl "github.com/OliveTin/OliveTin/internal/acl"
config "github.com/OliveTin/OliveTin/internal/config" config "github.com/OliveTin/OliveTin/internal/config"
"github.com/google/uuid"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"bytes" "bytes"
@ -15,18 +14,29 @@ import (
"time" "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 // ExecutionRequest is a request to execute an action. It's passed to an
// Executor. They're created from the grpcapi. // Executor. They're created from the grpcapi.
type ExecutionRequest struct { type ExecutionRequest struct {
ActionName string ActionName string
Arguments map[string]string Arguments map[string]string
UUID string
Tags []string Tags []string
action *config.Action action *config.Action
Cfg *config.Config Cfg *config.Config
AuthenticatedUser *acl.AuthenticatedUser AuthenticatedUser *acl.AuthenticatedUser
logEntry *InternalLogEntry logEntry *InternalLogEntry
finalParsedCommand string finalParsedCommand string
uuid string executor *Executor
} }
// InternalLogEntry objects are created by an Executor, and represent the final // InternalLogEntry objects are created by an Executor, and represent the final
@ -42,29 +52,21 @@ type InternalLogEntry struct {
TimedOut bool TimedOut bool
ExitCode int32 ExitCode int32
Tags []string Tags []string
ExecutionStarted bool
ExecutionCompleted 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 that logs are lightweight (so we don't need to have an action associated to
logs, etc. Therefore, we duplicate those values here. logs, etc. Therefore, we duplicate those values here.
*/ */
ActionTitle string ActionTitle string
ActionIcon string ActionIcon string
UUID string
ExecutionStarted bool
ExecutionCompleted bool
} }
type executorStepFunc func(*ExecutionRequest) bool type executorStepFunc func(*ExecutionRequest) bool
// Executor represents a helper class for executing commands. It's main method
// is ExecRequest
type Executor struct {
Logs map[string]*InternalLogEntry
chainOfCommand []executorStepFunc
}
// DefaultExecutor returns an Executor, with a sensible "chain of command" for // DefaultExecutor returns an Executor, with a sensible "chain of command" for
// executing actions. // executing actions.
func DefaultExecutor() *Executor { func DefaultExecutor() *Executor {
@ -72,23 +74,38 @@ func DefaultExecutor() *Executor {
e.Logs = make(map[string]*InternalLogEntry) e.Logs = make(map[string]*InternalLogEntry)
e.chainOfCommand = []executorStepFunc{ e.chainOfCommand = []executorStepFunc{
stepLogRequested,
stepFindAction, stepFindAction,
stepACLCheck, stepACLCheck,
stepParseArgs, stepParseArgs,
stepLogStart, stepLogStart,
stepExec, stepExec,
stepNotifyListeners,
stepLogFinish, stepLogFinish,
} }
return &e 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 // ExecRequest processes an ExecutionRequest
func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse { func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
req.uuid = uuid.New().String() // 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{ req.logEntry = &InternalLogEntry{
DatetimeStarted: time.Now().Format("2006-01-02 15:04:05"), DatetimeStarted: time.Now().Format("2006-01-02 15:04:05"),
ActionTitle: req.ActionName, ActionTitle: req.ActionName,
UUID: req.UUID,
Stdout: "", Stdout: "",
Stderr: "", Stderr: "",
ExitCode: -1337, // If an Action is not actually executed, this is the default exit code. ExitCode: -1337, // If an Action is not actually executed, this is the default exit code.
@ -96,12 +113,16 @@ func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
ExecutionCompleted: false, ExecutionCompleted: false,
} }
e.Logs[req.uuid] = req.logEntry e.Logs[req.UUID] = req.logEntry
for _, listener := range e.listeners {
listener.OnExecutionStarted(req.ActionName)
}
go e.execChain(req) go e.execChain(req)
return &pb.StartActionResponse{ return &pb.StartActionResponse{
ExecutionUuid: req.uuid, ExecutionUuid: req.UUID,
} }
} }
@ -152,9 +173,17 @@ func stepParseArgs(req *ExecutionRequest) bool {
return true return true
} }
func stepLogRequested(req *ExecutionRequest) bool {
log.WithFields(log.Fields{
"actionTitle": req.ActionName,
}).Infof("Action requested")
return true
}
func stepLogStart(req *ExecutionRequest) bool { func stepLogStart(req *ExecutionRequest) bool {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"title": req.action.Title, "actionTitle": req.action.Title,
"timeout": req.action.Timeout, "timeout": req.action.Timeout,
}).Infof("Action starting") }).Infof("Action starting")
@ -163,7 +192,7 @@ func stepLogStart(req *ExecutionRequest) bool {
func stepLogFinish(req *ExecutionRequest) bool { func stepLogFinish(req *ExecutionRequest) bool {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"title": req.action.Title, "actionTitle": req.action.Title,
"stdout": req.logEntry.Stdout, "stdout": req.logEntry.Stdout,
"stderr": req.logEntry.Stderr, "stderr": req.logEntry.Stderr,
"timedOut": req.logEntry.TimedOut, "timedOut": req.logEntry.TimedOut,
@ -173,6 +202,14 @@ func stepLogFinish(req *ExecutionRequest) bool {
return true return true
} }
func stepNotifyListeners(req *ExecutionRequest) bool {
for _, listener := range req.executor.listeners {
listener.OnExecutionFinished(req.logEntry)
}
return true
}
func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cmd { func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cmd {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
return exec.CommandContext(ctx, "cmd", "/C", finalParsedCommand) return exec.CommandContext(ctx, "cmd", "/C", finalParsedCommand)
@ -217,6 +254,7 @@ func stepExec(req *ExecutionRequest) bool {
} }
req.logEntry.Tags = req.Tags req.logEntry.Tags = req.Tags
req.logEntry.DatetimeFinished = time.Now().Format("2006-01-02 15:04:05")
return true return true
} }

View File

@ -6,8 +6,8 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"google.golang.org/grpc" "google.golang.org/grpc"
"io"
"net" "net"
"sort"
acl "github.com/OliveTin/OliveTin/internal/acl" acl "github.com/OliveTin/OliveTin/internal/acl"
config "github.com/OliveTin/OliveTin/internal/config" config "github.com/OliveTin/OliveTin/internal/config"
@ -20,7 +20,7 @@ var (
) )
type oliveTinAPI struct { type oliveTinAPI struct {
pb.UnimplementedOliveTinApiServer pb.UnimplementedOliveTinApiServiceServer
executor *executor.Executor executor *executor.Executor
} }
@ -36,6 +36,7 @@ func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *pb.StartActionRequest)
execReq := executor.ExecutionRequest{ execReq := executor.ExecutionRequest{
ActionName: req.ActionName, ActionName: req.ActionName,
UUID: req.Uuid,
Arguments: args, Arguments: args,
AuthenticatedUser: acl.UserFromContext(ctx, cfg), AuthenticatedUser: acl.UserFromContext(ctx, cfg),
Cfg: cfg, Cfg: cfg,
@ -44,6 +45,32 @@ func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *pb.StartActionRequest)
return api.executor.ExecRequest(&execReq), nil 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,
ExitCode: logEntry.ExitCode,
Tags: logEntry.Tags,
ExecutionUuid: logEntry.UUID,
}
return res, nil
}
/**
func (api *oliveTinAPI) WatchExecution(req *pb.WatchExecutionRequest, srv pb.OliveTinApi_WatchExecutionServer) error { func (api *oliveTinAPI) WatchExecution(req *pb.WatchExecutionRequest, srv pb.OliveTinApi_WatchExecutionServer) error {
log.Infof("Watch") log.Infof("Watch")
@ -69,6 +96,7 @@ func (api *oliveTinAPI) WatchExecution(req *pb.WatchExecutionRequest, srv pb.Oli
return nil return nil
} }
} }
*/
func (api *oliveTinAPI) GetDashboardComponents(ctx ctx.Context, req *pb.GetDashboardComponentsRequest) (*pb.GetDashboardComponentsResponse, error) { func (api *oliveTinAPI) GetDashboardComponents(ctx ctx.Context, req *pb.GetDashboardComponentsRequest) (*pb.GetDashboardComponentsResponse, error) {
user := acl.UserFromContext(ctx, cfg) user := acl.UserFromContext(ctx, cfg)
@ -104,6 +132,12 @@ func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *pb.GetLogsRequest) (*pb.Ge
}) })
} }
sorter := func(i, j int) bool {
return ret.Logs[i].DatetimeStarted < ret.Logs[j].DatetimeStarted
}
sort.Slice(ret.Logs, sorter);
return ret, nil return ret, nil
} }
@ -159,7 +193,7 @@ func Start(globalConfig *config.Config, ex *executor.Executor) {
} }
grpcServer := grpc.NewServer() grpcServer := grpc.NewServer()
pb.RegisterOliveTinApiServer(grpcServer, newServer(ex)) pb.RegisterOliveTinApiServiceServer(grpcServer, newServer(ex))
err = grpcServer.Serve(lis) err = grpcServer.Serve(lis)

View File

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

View File

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

View File

@ -10,6 +10,7 @@ away, and several other issues.
import ( import (
config "github.com/OliveTin/OliveTin/internal/config" config "github.com/OliveTin/OliveTin/internal/config"
"github.com/OliveTin/OliveTin/internal/websocket"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
@ -39,7 +40,7 @@ func StartSingleHTTPFrontend(cfg *config.Config) {
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.Header.Get("Connection"), "Upgrade") { if strings.Contains(r.Header.Get("Connection"), "Upgrade") {
handleWebsocket(w, r) websocket.HandleWebsocket(w, r)
} else { } else {
log.Debugf("ui req: %q", r.URL) log.Debugf("ui req: %q", r.URL)
webuiProxy.ServeHTTP(w, r) webuiProxy.ServeHTTP(w, r)

View File

@ -1,39 +0,0 @@
package httpservers
import (
"context"
log "github.com/sirupsen/logrus"
"net/http"
"nhooyr.io/websocket"
"nhooyr.io/websocket/wsjson"
"time"
)
func handleWebsocket(w http.ResponseWriter, r *http.Request) bool {
c, err := websocket.Accept(w, r, nil)
if err != nil {
log.Warnf("Websocket issue: %v", err)
return false
}
defer c.Close(websocket.StatusInternalError, "Goodbye")
ctx, cancel := context.WithTimeout(r.Context(), time.Second*10)
defer cancel()
var v interface{}
err = wsjson.Read(ctx, c, v)
log.Printf("recv: %v", v)
if err != nil {
log.Warnf("Websocket issue: %v", err)
return false
}
c.Close(websocket.StatusNormalClosure, "")
return true
}

View File

@ -0,0 +1,119 @@
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,
ExitCode: logEntry.ExitCode,
Tags: logEntry.Tags,
Uuid: logEntry.UUID,
}
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,58 @@
</main> </main>
<footer title = "footer"> <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> <p>
<a href = "https://docs.olivetin.app" target = "_new">Documentation</a> | <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> | <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> <a id = "available-version" href = "http://olivetin.app" target = "_blank" hidden>?</a>
</p> </p>
</footer> </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>
<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"> <template id = "tplArgumentForm">
<dialog>
<form class = "action-arguments"> <form class = "action-arguments">
<div class = "wrapper"> <div class = "wrapper">
<div> <div class = "action-header">
<span class = "icon" role = "icon"></span> <span class = "icon" role = "icon"></span>
<h2>Argument form</h2> <h2>Argument form</h2>
</div> </div>
@ -86,14 +125,20 @@
<button name = "cancel">Cancel</button> <button name = "cancel">Cancel</button>
</div> </div>
</div> </div>
<form> </form>
</button>
</template> </template>
<template id = "tplActionButton"> <template id = "tplActionButton">
<div>
<button> <button>
<span role = "icon" title = "button icon" class = "icon">&#x1f4a9;</span> <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> </button>
<div class = "executions">
</div>
</div>
</template> </template>
<template id = "tplLogRow"> <template id = "tplLogRow">
@ -129,7 +174,10 @@
to at least display a helpful error message if we can't use OliveTin. to at least display a helpful error message if we can't use OliveTin.
*/ */
function showBigError (type, friendlyType, message) { 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) console.error('Error ' + type + ': ', message)

View File

@ -1,5 +1,5 @@
import { marshalLogsJsonToHtml } from './marshaller.js'
import './ArgumentForm.js' import './ArgumentForm.js'
import './ExecutionButton.js'
class ActionButton extends window.HTMLElement { class ActionButton extends window.HTMLElement {
constructFromJson (json) { constructFromJson (json) {
@ -25,6 +25,7 @@ class ActionButton extends window.HTMLElement {
}) })
document.body.appendChild(frm) document.body.appendChild(frm)
frm.querySelector('dialog').showModal()
} else { } else {
this.startAction() this.startAction()
} }
@ -32,7 +33,8 @@ class ActionButton extends window.HTMLElement {
this.updateFromJson(json) this.updateFromJson(json)
this.updateDom() this.domTitle.innerText = this.btn.title
this.domIcon.innerHTML = this.unicodeIcon
this.setAttribute('id', 'actionButton_' + json.id) this.setAttribute('id', 'actionButton_' + json.id)
} }
@ -52,20 +54,27 @@ class ActionButton extends window.HTMLElement {
} }
startAction (actionArgs) { startAction (actionArgs) {
this.btn.disabled = true // this.btn.disabled = true
this.isWaiting = true // this.isWaiting = true
this.updateDom() // this.updateDom()
this.btn.classList = [] // Removes old animation classes this.btn.classList = [] // Removes old animation classes
if (actionArgs === undefined) { if (actionArgs === undefined) {
actionArgs = [] 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 = { const startActionArgs = {
actionName: this.btn.title, 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, { window.fetch(this.actionCallUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
@ -80,45 +89,12 @@ class ActionButton extends window.HTMLElement {
} }
} }
).then((json) => { ).then((json) => {
marshalLogsJsonToHtml({ logs: [json.logEntry] }) // The button used to wait for the action to finish, but now it is fire & forget
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!')
}
}).catch(err => { }).catch(err => {
this.onActionError(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 () { constructDomFromTemplate () {
const tpl = document.getElementById('tplActionButton') const tpl = document.getElementById('tplActionButton')
const content = tpl.content.cloneNode(true) const content = tpl.content.cloneNode(true)
@ -134,27 +110,6 @@ class ActionButton extends window.HTMLElement {
this.domTitle = this.btn.querySelector('.title') this.domTitle = this.btn.querySelector('.title')
this.domIcon = this.btn.querySelector('.icon') 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) window.customElements.define('action-button', ActionButton)

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

@ -0,0 +1,106 @@
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.exitCode === -1337) {
this.onActionError('Error')
} else if (LogEntry.exitCode !== 0) {
this.onActionResult('action-nonzero-exit', 'Exit code ' + LogEntry.exitCode)
} else {
this.onActionResult('action-success', 'Success!')
}
}
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)
}
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,58 @@
// 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')
}
show () {
this.dlg.showModal()
}
renderResult (res) {
this.executionUuid = res.logEntry.executionUuid
if (res.logEntry.datetimeFinished === '') {
this.domExitCode.innerText = 'Still running...'
this.domDatetimeFinished.innerText = 'Still running...'
} else {
if (res.logEntry.timedOut) {
this.domExitCode.innerText = 'Timed out'
} else {
this.domExitCode.innerText = res.logEntry.exitCode
}
this.domDatetimeFinished.innerText = res.logEntry.datetimeFinished
}
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)' logTableExitCode += ' (timed out)'
} }
row.querySelector('.timestamp').innerText = logEntry.datetime row.querySelector('.timestamp').innerText = logEntry.datetimeStarted
row.querySelector('.content').innerText = logEntry.actionTitle row.querySelector('.content').innerText = logEntry.actionTitle
row.querySelector('.icon').innerHTML = logEntry.actionIcon row.querySelector('.icon').innerHTML = logEntry.actionIcon
row.querySelector('pre.stdout').innerText = logEntry.stdout row.querySelector('pre.stdout').innerText = logEntry.stdout

View File

@ -1,4 +1,14 @@
export function setupWebsocket () { 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 window.websocketAvailable = false
let proto = 'ws:' let proto = 'ws:'
@ -18,20 +28,44 @@ export function setupWebsocket () {
function websocketOnOpen (evt) { function websocketOnOpen (evt) {
window.websocketAvailable = true window.websocketAvailable = true
console.log('open')
const foo = '{}' window.ws.send('monitor')
window.ws.send(foo) window.refreshLoop()
} }
function websocketOnMessage (msg) { function websocketOnMessage (msg) {
console.log(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) { function websocketOnError (err) {
window.websocketAvailable = false window.websocketAvailable = false
console.log(err) window.refreshLoop()
console.error(err)
} }
function websocketOnClose () { function websocketOnClose () {

View File

@ -1,7 +1,7 @@
'use strict' 'use strict'
import { marshalActionButtonsJsonToHtml, marshalLogsJsonToHtml } from './js/marshaller.js' import { marshalActionButtonsJsonToHtml, marshalLogsJsonToHtml } from './js/marshaller.js'
import { setupWebsocket } from './js/websocket.js' import { checkWebsocketConnection } from './js/websocket.js'
function showSection (name) { function showSection (name) {
for (const otherName of ['Actions', 'Logs']) { for (const otherName of ['Actions', 'Logs']) {
@ -20,15 +20,42 @@ function setupSections () {
showSection('Actions') 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 () { function fetchGetDashboardComponents () {
window.fetch(window.restBaseUrl + 'GetDashboardComponents', { window.fetch(window.restBaseUrl + 'GetDashboardComponents', {
cors: 'cors' cors: 'cors'
}).then(res => { }).then(res => {
return res.json() return res.json()
}).then(res => { }).then(res => {
window.restAvailable = true
marshalActionButtonsJsonToHtml(res) marshalActionButtonsJsonToHtml(res)
}).catch(err => { }).catch(() => { // err is 1st arg
window.showBigError('fetch-buttons', 'getting buttons', err, 'blat') window.restAvailable = false
// window.showBigError('fetch-buttons', 'getting buttons', err, 'blat')
}) })
} }
@ -56,7 +83,7 @@ function processWebuiSettingsJson (settings) {
document.head.appendChild(themeCss) document.head.appendChild(themeCss)
} }
document.querySelector('#currentVersion').innerText = 'Version: ' + settings.CurrentVersion document.querySelector('#currentVersion').innerText = settings.CurrentVersion
if (settings.ShowNewVersions && settings.AvailableVersion !== 'none') { if (settings.ShowNewVersions && settings.AvailableVersion !== 'none') {
document.querySelector('#available-version').innerText = 'New Version Available: ' + settings.AvailableVersion document.querySelector('#available-version').innerText = 'New Version Available: ' + settings.AvailableVersion
@ -76,17 +103,16 @@ function processWebuiSettingsJson (settings) {
function main () { function main () {
setupSections() setupSections()
setupWebsocket()
window.fetch('webUiSettings.json').then(res => { window.fetch('webUiSettings.json').then(res => {
return res.json() return res.json()
}).then(res => { }).then(res => {
processWebuiSettingsJson(res) processWebuiSettingsJson(res)
fetchGetDashboardComponents() window.restAvailable = true
fetchGetLogs() window.refreshLoop = refreshLoop
window.refreshLoop()
window.buttonInterval = setInterval(fetchGetDashboardComponents, 3000) setInterval(refreshLoop, 3000)
}).catch(err => { }).catch(err => {
window.showBigError('fetch-webui-settings', 'getting webui settings', err) window.showBigError('fetch-webui-settings', 'getting webui settings', err)
}) })

View File

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