diff --git a/OliveTin.proto b/OliveTin.proto index 49e0946..3ae3e4e 100644 --- a/OliveTin.proto +++ b/OliveTin.proto @@ -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 popupOnStart = 6; + 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,24 +57,25 @@ message StartActionArgument { } message StartActionResponse { - string executionUuid = 2; + string execution_uuid = 2; } message GetLogsRequest{}; message LogEntry { - string datetimeStarted = 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 executionUuid = 11; - string datetimeFinished = 12; + string execution_uuid = 11; + string datetime_finished = 12; + string uuid = 13; } message GetLogsResponse { @@ -90,17 +93,25 @@ message ValidateArgumentTypeResponse { } message WatchExecutionRequest { - string executionUuid = 1; + 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 {} @@ -109,7 +120,7 @@ message SosReportResponse { string alert = 1; } -service OliveTinApi { +service OliveTinApiService { rpc GetDashboardComponents(GetDashboardComponentsRequest) returns (GetDashboardComponentsResponse) { option (google.api.http) = { get: "/api/GetDashboardComponents" @@ -123,9 +134,9 @@ service OliveTinApi { }; } - rpc WatchExecution(WatchExecutionRequest) returns (stream WatchExecutionUpdate) { + rpc ExecutionStatus(ExecutionStatusRequest) returns (ExecutionStatusResponse) { option (google.api.http) = { - post: "/api/WatchExecution" + post: "/api/ExecutionStatus" body: "*" }; } diff --git a/cmd/OliveTin/main.go b/cmd/OliveTin/main.go index 04dfb17..c6ca646 100644 --- a/cmd/OliveTin/main.go +++ b/cmd/OliveTin/main.go @@ -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) diff --git a/go.mod b/go.mod index 4d66088..0a62723 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( 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/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.3 @@ -20,7 +21,6 @@ require ( google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 google.golang.org/protobuf v1.31.0 gopkg.in/yaml.v3 v3.0.1 - nhooyr.io/websocket v1.8.7 ) require ( diff --git a/go.sum b/go.sum index 17147d5..16728f3 100644 --- a/go.sum +++ b/go.sum @@ -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/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= 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/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 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/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU= 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/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus= github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig= -github.com/gobwas/httphead v0.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/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M= 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-containerregistry v0.16.1 h1:rUEt426sR6nyrL3gt+18ibRcvYpKYdpsa5ZW7MA08dQ= 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/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= @@ -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/go.mod h1:Zi/ZFkEqFHTm7qkjyNJjaWH4LQA9LQhGJyF0lTYGpxw= 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.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.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/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 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/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 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/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/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.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 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/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 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.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 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/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/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/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk= 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-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-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-20200202164722-d101bd2416d5/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/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 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-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= -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/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/acl/acl.go b/internal/acl/acl.go index a91e399..8aa7222 100644 --- a/internal/acl/acl.go +++ b/internal/acl/acl.go @@ -90,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 } diff --git a/internal/executor/arguments.go b/internal/executor/arguments.go index da97ebd..a82a30d 100644 --- a/internal/executor/arguments.go +++ b/internal/executor/arguments.go @@ -22,8 +22,9 @@ var ( func parseActionArguments(rawShellCommand string, values map[string]string, action *config.Action) (string, error) { log.WithFields(log.Fields{ - "cmd": rawShellCommand, - }).Infof("Before Parse Args") + "actionTitle": action.Title, + "cmd": rawShellCommand, + }).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{ - "cmd": rawShellCommand, - }).Infof("After Parse Args") + "actionTitle": action.Title, + "cmd": rawShellCommand, + }).Infof("Action parse args - After") return rawShellCommand, nil } diff --git a/internal/executor/executor.go b/internal/executor/executor.go index ef652e1..8277516 100644 --- a/internal/executor/executor.go +++ b/internal/executor/executor.go @@ -4,7 +4,6 @@ import ( pb "github.com/OliveTin/OliveTin/gen/grpc" acl "github.com/OliveTin/OliveTin/internal/acl" config "github.com/OliveTin/OliveTin/internal/config" - "github.com/google/uuid" log "github.com/sirupsen/logrus" "bytes" @@ -15,56 +14,59 @@ import ( "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 - uuid 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 { - DatetimeStarted string - DatetimeFinished string - Stdout string - Stderr string - StdoutBuffer io.ReadCloser - StderrBuffer io.ReadCloser - TimedOut bool - ExitCode int32 - Tags []string + DatetimeStarted string + DatetimeFinished string + Stdout string + Stderr string + StdoutBuffer io.ReadCloser + StderrBuffer io.ReadCloser + TimedOut bool + ExitCode int32 + 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 logs, etc. Therefore, we duplicate those values here. */ ActionTitle string ActionIcon string - - ExecutionStarted bool - ExecutionCompleted bool + 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 map[string]*InternalLogEntry - - chainOfCommand []executorStepFunc -} - // DefaultExecutor returns an Executor, with a sensible "chain of command" for // executing actions. func DefaultExecutor() *Executor { @@ -72,23 +74,38 @@ func DefaultExecutor() *Executor { e.Logs = make(map[string]*InternalLogEntry) e.chainOfCommand = []executorStepFunc{ + stepLogRequested, stepFindAction, stepACLCheck, stepParseArgs, stepLogStart, stepExec, + stepNotifyListeners, stepLogFinish, } 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 = 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{ 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. @@ -96,12 +113,16 @@ func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse { 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) return &pb.StartActionResponse{ - ExecutionUuid: req.uuid, + ExecutionUuid: req.UUID, } } @@ -152,10 +173,18 @@ 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, - "timeout": req.action.Timeout, + "actionTitle": req.action.Title, + "timeout": req.action.Timeout, }).Infof("Action starting") return true @@ -163,16 +192,24 @@ func stepLogStart(req *ExecutionRequest) bool { func stepLogFinish(req *ExecutionRequest) bool { log.WithFields(log.Fields{ - "title": req.action.Title, - "stdout": req.logEntry.Stdout, - "stderr": req.logEntry.Stderr, - "timedOut": req.logEntry.TimedOut, - "exit": req.logEntry.ExitCode, + "actionTitle": req.action.Title, + "stdout": req.logEntry.Stdout, + "stderr": req.logEntry.Stderr, + "timedOut": req.logEntry.TimedOut, + "exit": req.logEntry.ExitCode, }).Infof("Action finished") 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 { if runtime.GOOS == "windows" { return exec.CommandContext(ctx, "cmd", "/C", finalParsedCommand) @@ -217,6 +254,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 } diff --git a/internal/grpcapi/grpcApi.go b/internal/grpcapi/grpcApi.go index 65a5f6f..0c4dc0b 100644 --- a/internal/grpcapi/grpcApi.go +++ b/internal/grpcapi/grpcApi.go @@ -6,8 +6,8 @@ import ( log "github.com/sirupsen/logrus" "google.golang.org/grpc" - "io" "net" + "sort" acl "github.com/OliveTin/OliveTin/internal/acl" config "github.com/OliveTin/OliveTin/internal/config" @@ -20,7 +20,7 @@ var ( ) type oliveTinAPI struct { - pb.UnimplementedOliveTinApiServer + pb.UnimplementedOliveTinApiServiceServer executor *executor.Executor } @@ -36,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, @@ -44,6 +45,32 @@ 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, + ExitCode: logEntry.ExitCode, + Tags: logEntry.Tags, + ExecutionUuid: logEntry.UUID, + } + + return res, nil +} + +/** func (api *oliveTinAPI) WatchExecution(req *pb.WatchExecutionRequest, srv pb.OliveTinApi_WatchExecutionServer) error { log.Infof("Watch") @@ -69,6 +96,7 @@ func (api *oliveTinAPI) WatchExecution(req *pb.WatchExecutionRequest, srv pb.Oli return nil } } +*/ func (api *oliveTinAPI) GetDashboardComponents(ctx ctx.Context, req *pb.GetDashboardComponentsRequest) (*pb.GetDashboardComponentsResponse, error) { 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 } @@ -159,7 +193,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) diff --git a/internal/grpcapi/grpcApi_test.go b/internal/grpcapi/grpcApi_test.go index 9c95e72..646db2b 100644 --- a/internal/grpcapi/grpcApi_test.go +++ b/internal/grpcapi/grpcApi_test.go @@ -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 } diff --git a/internal/httpservers/restapi.go b/internal/httpservers/restapi.go index cf200bb..bbc85be 100644 --- a/internal/httpservers/restapi.go +++ b/internal/httpservers/restapi.go @@ -80,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, }, }, @@ -88,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) diff --git a/internal/httpservers/singleFrontend.go b/internal/httpservers/singleFrontend.go index cb39c82..8c76ce4 100644 --- a/internal/httpservers/singleFrontend.go +++ b/internal/httpservers/singleFrontend.go @@ -10,6 +10,7 @@ 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" @@ -39,7 +40,7 @@ func StartSingleHTTPFrontend(cfg *config.Config) { mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.Header.Get("Connection"), "Upgrade") { - handleWebsocket(w, r) + websocket.HandleWebsocket(w, r) } else { log.Debugf("ui req: %q", r.URL) webuiProxy.ServeHTTP(w, r) diff --git a/internal/httpservers/websocket.go b/internal/httpservers/websocket.go deleted file mode 100644 index 17e922c..0000000 --- a/internal/httpservers/websocket.go +++ /dev/null @@ -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 -} diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go new file mode 100644 index 0000000..23b0061 --- /dev/null +++ b/internal/websocket/websocket.go @@ -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) + + // + // 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("}")...) + // + + 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 +} diff --git a/webui/index.html b/webui/index.html index 4fd8298..b00beab 100644 --- a/webui/index.html +++ b/webui/index.html @@ -62,38 +62,83 @@ + +
+ + +

Log: + ? +

+
+

+ Started: unknown + Finished: unknown +

+

+ Exit Code: unknown +

+ +
+ stdout +
+					?
+				
+
+ +
+ stderr +
+					?
+				
+
+ +
+ +
+
+