feature: log-popup
This commit is contained in:
parent
0b75b3847d
commit
89f08ae6c7
|
|
@ -9,8 +9,8 @@ message Action {
|
||||||
string title = 2;
|
string title = 2;
|
||||||
string icon = 3;
|
string icon = 3;
|
||||||
bool canExec = 4;
|
bool canExec = 4;
|
||||||
|
|
||||||
repeated ActionArgument arguments = 5;
|
repeated ActionArgument arguments = 5;
|
||||||
|
bool popupOnStart = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ActionArgument {
|
message ActionArgument {
|
||||||
|
|
@ -55,13 +55,13 @@ message StartActionArgument {
|
||||||
}
|
}
|
||||||
|
|
||||||
message StartActionResponse {
|
message StartActionResponse {
|
||||||
LogEntry logEntry = 1;
|
string executionUuid = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetLogsRequest{};
|
message GetLogsRequest{};
|
||||||
|
|
||||||
message LogEntry {
|
message LogEntry {
|
||||||
string datetime = 1;
|
string datetimeStarted = 1;
|
||||||
string actionTitle = 2;
|
string actionTitle = 2;
|
||||||
string stdout = 3;
|
string stdout = 3;
|
||||||
string stderr = 4;
|
string stderr = 4;
|
||||||
|
|
@ -71,6 +71,8 @@ message LogEntry {
|
||||||
string userClass = 8;
|
string userClass = 8;
|
||||||
string actionIcon = 9;
|
string actionIcon = 9;
|
||||||
repeated string tags = 10;
|
repeated string tags = 10;
|
||||||
|
string executionUuid = 11;
|
||||||
|
string datetimeFinished = 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetLogsResponse {
|
message GetLogsResponse {
|
||||||
|
|
@ -87,6 +89,14 @@ message ValidateArgumentTypeResponse {
|
||||||
string description = 2;
|
string description = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message WatchExecutionRequest {
|
||||||
|
string executionUuid = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WatchExecutionUpdate {
|
||||||
|
string update = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message WhoAmIRequest {}
|
message WhoAmIRequest {}
|
||||||
|
|
||||||
message WhoAmIResponse {
|
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) {
|
rpc GetLogs(GetLogsRequest) returns (GetLogsResponse) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
get: "/api/GetLogs"
|
get: "/api/GetLogs"
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,10 @@ func getMetdataKeyOrEmpty(md metadata.MD, key string) string {
|
||||||
func UserFromContext(ctx context.Context, cfg *config.Config) *AuthenticatedUser {
|
func UserFromContext(ctx context.Context, cfg *config.Config) *AuthenticatedUser {
|
||||||
md, ok := metadata.FromIncomingContext(ctx)
|
md, ok := metadata.FromIncomingContext(ctx)
|
||||||
|
|
||||||
ret := &AuthenticatedUser{}
|
ret := &AuthenticatedUser{
|
||||||
|
Username: "guest",
|
||||||
|
Usergroup: "guest",
|
||||||
|
}
|
||||||
|
|
||||||
if ok {
|
if ok {
|
||||||
ret.Username = getMetdataKeyOrEmpty(md, "username")
|
ret.Username = getMetdataKeyOrEmpty(md, "username")
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ type Action struct {
|
||||||
ExecOnStartup bool
|
ExecOnStartup bool
|
||||||
ExecOnCron []string
|
ExecOnCron []string
|
||||||
Arguments []ActionArgument
|
Arguments []ActionArgument
|
||||||
|
PopupOnStart bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActionArgument objects appear on Actions.
|
// ActionArgument objects appear on Actions.
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,12 @@ 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"
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -24,18 +26,22 @@ type ExecutionRequest struct {
|
||||||
AuthenticatedUser *acl.AuthenticatedUser
|
AuthenticatedUser *acl.AuthenticatedUser
|
||||||
logEntry *InternalLogEntry
|
logEntry *InternalLogEntry
|
||||||
finalParsedCommand string
|
finalParsedCommand string
|
||||||
|
uuid string
|
||||||
}
|
}
|
||||||
|
|
||||||
// InternalLogEntry objects are created by an Executor, and represent the final
|
// 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
|
// state of execution (even if the command is not executed). It's designed to be
|
||||||
// easily serializable.
|
// easily serializable.
|
||||||
type InternalLogEntry struct {
|
type InternalLogEntry struct {
|
||||||
Datetime string
|
DatetimeStarted string
|
||||||
Stdout string
|
DatetimeFinished string
|
||||||
Stderr string
|
Stdout string
|
||||||
TimedOut bool
|
Stderr string
|
||||||
ExitCode int32
|
StdoutBuffer io.ReadCloser
|
||||||
Tags []string
|
StderrBuffer io.ReadCloser
|
||||||
|
TimedOut bool
|
||||||
|
ExitCode int32
|
||||||
|
Tags []string
|
||||||
|
|
||||||
/*
|
/*
|
||||||
The following two properties are obviously on Action normally, but it's useful
|
The following two properties are obviously on Action normally, but it's useful
|
||||||
|
|
@ -44,6 +50,9 @@ type InternalLogEntry struct {
|
||||||
*/
|
*/
|
||||||
ActionTitle string
|
ActionTitle string
|
||||||
ActionIcon string
|
ActionIcon string
|
||||||
|
|
||||||
|
ExecutionStarted bool
|
||||||
|
ExecutionCompleted bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type executorStepFunc func(*ExecutionRequest) 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
|
// Executor represents a helper class for executing commands. It's main method
|
||||||
// is ExecRequest
|
// is ExecRequest
|
||||||
type Executor struct {
|
type Executor struct {
|
||||||
Logs []InternalLogEntry
|
Logs map[string]*InternalLogEntry
|
||||||
|
|
||||||
chainOfCommand []executorStepFunc
|
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
|
// DefaultExecutor returns an Executor, with a sensible "chain of command" for
|
||||||
// executing actions.
|
// executing actions.
|
||||||
func DefaultExecutor() *Executor {
|
func DefaultExecutor() *Executor {
|
||||||
e := Executor{}
|
e := Executor{}
|
||||||
|
e.Logs = make(map[string]*InternalLogEntry)
|
||||||
|
|
||||||
e.chainOfCommand = []executorStepFunc{
|
e.chainOfCommand = []executorStepFunc{
|
||||||
stepFindAction,
|
stepFindAction,
|
||||||
stepACLCheck,
|
stepACLCheck,
|
||||||
|
|
@ -103,6 +83,36 @@ func DefaultExecutor() *Executor {
|
||||||
return &e
|
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 {
|
func stepFindAction(req *ExecutionRequest) bool {
|
||||||
actualAction := req.Cfg.FindAction(req.ActionName)
|
actualAction := req.Cfg.FindAction(req.ActionName)
|
||||||
|
|
||||||
|
|
@ -181,9 +191,19 @@ func stepExec(req *ExecutionRequest) bool {
|
||||||
cmd := wrapCommandInShell(ctx, req.finalParsedCommand)
|
cmd := wrapCommandInShell(ctx, req.finalParsedCommand)
|
||||||
cmd.Stdout = &stdout
|
cmd.Stdout = &stdout
|
||||||
cmd.Stderr = &stderr
|
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.ExitCode = int32(cmd.ProcessState.ExitCode())
|
||||||
req.logEntry.Stdout = stdout.String()
|
req.logEntry.Stdout = stdout.String()
|
||||||
req.logEntry.Stderr = stderr.String()
|
req.logEntry.Stderr = stderr.String()
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
acl "github.com/OliveTin/OliveTin/internal/acl"
|
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
|
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) {
|
func (api *oliveTinAPI) GetDashboardComponents(ctx ctx.Context, req *pb.GetDashboardComponentsRequest) (*pb.GetDashboardComponentsResponse, error) {
|
||||||
user := acl.UserFromContext(ctx, cfg)
|
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.
|
// 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{
|
ret.Logs = append(ret.Logs, &pb.LogEntry{
|
||||||
ActionTitle: logEntry.ActionTitle,
|
ActionTitle: logEntry.ActionTitle,
|
||||||
ActionIcon: logEntry.ActionIcon,
|
ActionIcon: logEntry.ActionIcon,
|
||||||
Datetime: logEntry.Datetime,
|
DatetimeStarted: logEntry.DatetimeStarted,
|
||||||
Stdout: logEntry.Stdout,
|
DatetimeFinished: logEntry.DatetimeFinished,
|
||||||
Stderr: logEntry.Stderr,
|
Stdout: logEntry.Stdout,
|
||||||
TimedOut: logEntry.TimedOut,
|
Stderr: logEntry.Stderr,
|
||||||
ExitCode: logEntry.ExitCode,
|
TimedOut: logEntry.TimedOut,
|
||||||
Tags: logEntry.Tags,
|
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 {
|
func actionCfgToPb(action config.Action, user *acl.AuthenticatedUser) *pb.Action {
|
||||||
btn := pb.Action{
|
btn := pb.Action{
|
||||||
Id: fmt.Sprintf("%x", md5.Sum([]byte(action.Title))),
|
Id: fmt.Sprintf("%x", md5.Sum([]byte(action.Title))),
|
||||||
Title: action.Title,
|
Title: action.Title,
|
||||||
Icon: action.Icon,
|
Icon: action.Icon,
|
||||||
CanExec: acl.IsAllowedExec(cfg, user, &action),
|
CanExec: acl.IsAllowedExec(cfg, user, &action),
|
||||||
|
PopupOnStart: action.PopupOnStart,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cfgArg := range action.Arguments {
|
for _, cfgArg := range action.Arguments {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue