feature: log-popup

This commit is contained in:
jamesread 2023-03-23 22:33:40 +00:00
parent 0b75b3847d
commit 89f08ae6c7
6 changed files with 128 additions and 57 deletions

View File

@ -9,13 +9,13 @@ message Action {
string title = 2;
string icon = 3;
bool canExec = 4;
repeated ActionArgument arguments = 5;
bool popupOnStart = 6;
}
message ActionArgument {
string name = 1;
string title = 2;
string title = 2;
string type = 3;
string defaultValue = 4;
@ -55,13 +55,13 @@ message StartActionArgument {
}
message StartActionResponse {
LogEntry logEntry = 1;
string executionUuid = 2;
}
message GetLogsRequest{};
message LogEntry {
string datetime = 1;
string datetimeStarted = 1;
string actionTitle = 2;
string stdout = 3;
string stderr = 4;
@ -71,6 +71,8 @@ message LogEntry {
string userClass = 8;
string actionIcon = 9;
repeated string tags = 10;
string executionUuid = 11;
string datetimeFinished = 12;
}
message GetLogsResponse {
@ -87,6 +89,14 @@ message ValidateArgumentTypeResponse {
string description = 2;
}
message WatchExecutionRequest {
string executionUuid = 1;
}
message WatchExecutionUpdate {
string update = 1;
}
message WhoAmIRequest {}
message WhoAmIResponse {
@ -113,6 +123,13 @@ service OliveTinApi {
};
}
rpc WatchExecution(WatchExecutionRequest) returns (stream WatchExecutionUpdate) {
option (google.api.http) = {
post: "/api/WatchExecution"
body: "*"
};
}
rpc GetLogs(GetLogsRequest) returns (GetLogsResponse) {
option (google.api.http) = {
get: "/api/GetLogs"

View File

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

View File

@ -13,6 +13,7 @@ type Action struct {
ExecOnStartup bool
ExecOnCron []string
Arguments []ActionArgument
PopupOnStart bool
}
// ActionArgument objects appear on Actions.

View File

@ -4,10 +4,12 @@ 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"
"context"
"io"
"os/exec"
"runtime"
"time"
@ -24,18 +26,22 @@ type ExecutionRequest struct {
AuthenticatedUser *acl.AuthenticatedUser
logEntry *InternalLogEntry
finalParsedCommand string
uuid string
}
// InternalLogEntry objects are created by an Executor, and represent the final
// state of execution (even if the command is not executed). It's designed to be
// easily serializable.
type InternalLogEntry struct {
Datetime string
Stdout string
Stderr string
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
/*
The following two properties are obviously on Action normally, but it's useful
@ -44,6 +50,9 @@ type InternalLogEntry struct {
*/
ActionTitle string
ActionIcon string
ExecutionStarted bool
ExecutionCompleted bool
}
type executorStepFunc func(*ExecutionRequest) bool
@ -51,46 +60,17 @@ type executorStepFunc func(*ExecutionRequest) bool
// Executor represents a helper class for executing commands. It's main method
// is ExecRequest
type Executor struct {
Logs []InternalLogEntry
Logs map[string]*InternalLogEntry
chainOfCommand []executorStepFunc
}
// ExecRequest processes an ExecutionRequest
func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
req.logEntry = &InternalLogEntry{
Datetime: time.Now().Format("2006-01-02 15:04:05"),
ActionTitle: req.ActionName,
Stdout: "",
Stderr: "",
ExitCode: -1337, // If an Action is not actually executed, this is the default exit code.
}
for _, step := range e.chainOfCommand {
if !step(req) {
break
}
}
e.Logs = append(e.Logs, *req.logEntry)
return &pb.StartActionResponse{
LogEntry: &pb.LogEntry{
ActionTitle: req.logEntry.ActionTitle,
ActionIcon: req.logEntry.ActionIcon,
Datetime: req.logEntry.Datetime,
Stderr: req.logEntry.Stderr,
Stdout: req.logEntry.Stdout,
TimedOut: req.logEntry.TimedOut,
ExitCode: req.logEntry.ExitCode,
},
}
}
// DefaultExecutor returns an Executor, with a sensible "chain of command" for
// executing actions.
func DefaultExecutor() *Executor {
e := Executor{}
e.Logs = make(map[string]*InternalLogEntry)
e.chainOfCommand = []executorStepFunc{
stepFindAction,
stepACLCheck,
@ -103,6 +83,36 @@ func DefaultExecutor() *Executor {
return &e
}
// ExecRequest processes an ExecutionRequest
func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
req.uuid = uuid.New().String()
req.logEntry = &InternalLogEntry{
DatetimeStarted: time.Now().Format("2006-01-02 15:04:05"),
ActionTitle: req.ActionName,
Stdout: "",
Stderr: "",
ExitCode: -1337, // If an Action is not actually executed, this is the default exit code.
ExecutionStarted: false,
ExecutionCompleted: false,
}
e.Logs[req.uuid] = req.logEntry
go e.execChain(req)
return &pb.StartActionResponse{
ExecutionUuid: req.uuid,
}
}
func (e *Executor) execChain(req *ExecutionRequest) {
for _, step := range e.chainOfCommand {
if !step(req) {
break
}
}
}
func stepFindAction(req *ExecutionRequest) bool {
actualAction := req.Cfg.FindAction(req.ActionName)
@ -181,9 +191,19 @@ func stepExec(req *ExecutionRequest) bool {
cmd := wrapCommandInShell(ctx, req.finalParsedCommand)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
req.logEntry.StdoutBuffer, _ = cmd.StdoutPipe()
req.logEntry.StderrBuffer, _ = cmd.StderrPipe()
runerr := cmd.Run()
req.logEntry.ExecutionStarted = true
runerr := cmd.Start()
cmd.Wait()
//req.logEntry.Stdout = req.logEntry.StdoutBuffer.String()
//req.logEntry.Stderr = req.logEntry.StderrBuffer.String()
req.logEntry.ExecutionCompleted = true
req.logEntry.ExitCode = int32(cmd.ProcessState.ExitCode())
req.logEntry.Stdout = stdout.String()
req.logEntry.Stderr = stderr.String()

View File

@ -6,6 +6,7 @@ import (
log "github.com/sirupsen/logrus"
"google.golang.org/grpc"
"io"
"net"
acl "github.com/OliveTin/OliveTin/internal/acl"
@ -43,6 +44,32 @@ func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *pb.StartActionRequest)
return api.executor.ExecRequest(&execReq), nil
}
func (api *oliveTinAPI) WatchExecution(req *pb.WatchExecutionRequest, srv pb.OliveTinApi_WatchExecutionServer) error {
log.Infof("Watch")
if logEntry, ok := api.executor.Logs[req.ExecutionUuid]; !ok {
log.Errorf("Execution not found: %v", req.ExecutionUuid)
return nil
} else {
if logEntry.ExecutionStarted {
for !logEntry.ExecutionCompleted {
tmp := make([]byte, 256)
red, err := io.ReadAtLeast(logEntry.StdoutBuffer, tmp, 1)
log.Infof("%v %v", red, err)
srv.Send(&pb.WatchExecutionUpdate{
Update: string(tmp),
})
}
}
return nil
}
}
func (api *oliveTinAPI) GetDashboardComponents(ctx ctx.Context, req *pb.GetDashboardComponentsRequest) (*pb.GetDashboardComponentsResponse, error) {
user := acl.UserFromContext(ctx, cfg)
@ -62,16 +89,18 @@ func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *pb.GetLogsRequest) (*pb.Ge
// TODO Limit to 10 entries or something to prevent browser lag.
for _, logEntry := range api.executor.Logs {
for uuid, logEntry := range api.executor.Logs {
ret.Logs = append(ret.Logs, &pb.LogEntry{
ActionTitle: logEntry.ActionTitle,
ActionIcon: logEntry.ActionIcon,
Datetime: logEntry.Datetime,
Stdout: logEntry.Stdout,
Stderr: logEntry.Stderr,
TimedOut: logEntry.TimedOut,
ExitCode: logEntry.ExitCode,
Tags: logEntry.Tags,
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: uuid,
})
}

View File

@ -25,10 +25,11 @@ func actionsCfgToPb(cfgActions []config.Action, user *acl.AuthenticatedUser) *pb
func actionCfgToPb(action config.Action, user *acl.AuthenticatedUser) *pb.Action {
btn := pb.Action{
Id: fmt.Sprintf("%x", md5.Sum([]byte(action.Title))),
Title: action.Title,
Icon: action.Icon,
CanExec: acl.IsAllowedExec(cfg, user, &action),
Id: fmt.Sprintf("%x", md5.Sum([]byte(action.Title))),
Title: action.Title,
Icon: action.Icon,
CanExec: acl.IsAllowedExec(cfg, user, &action),
PopupOnStart: action.PopupOnStart,
}
for _, cfgArg := range action.Arguments {