feature: log-popup
This commit is contained in:
parent
0b75b3847d
commit
89f08ae6c7
|
|
@ -9,8 +9,8 @@ message Action {
|
|||
string title = 2;
|
||||
string icon = 3;
|
||||
bool canExec = 4;
|
||||
|
||||
repeated ActionArgument arguments = 5;
|
||||
bool popupOnStart = 6;
|
||||
}
|
||||
|
||||
message ActionArgument {
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ type Action struct {
|
|||
ExecOnStartup bool
|
||||
ExecOnCron []string
|
||||
Arguments []ActionArgument
|
||||
PopupOnStart bool
|
||||
}
|
||||
|
||||
// ActionArgument objects appear on Actions.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue