parent
e5a870ed94
commit
e8cb661938
|
|
@ -9,3 +9,4 @@ reports
|
|||
releases/
|
||||
dist/
|
||||
installation-id.txt
|
||||
tmp/
|
||||
|
|
|
|||
|
|
@ -76,6 +76,9 @@ message LogEntry {
|
|||
string execution_uuid = 11;
|
||||
string datetime_finished = 12;
|
||||
string uuid = 13;
|
||||
bool execution_started = 14;
|
||||
bool execution_finished = 15;
|
||||
bool blocked = 16;
|
||||
}
|
||||
|
||||
message GetLogsResponse {
|
||||
|
|
|
|||
|
|
@ -13,13 +13,18 @@ actions:
|
|||
# This will run a simple script that you create.
|
||||
- title: Run backup script
|
||||
shell: /opt/backupScript.sh
|
||||
maxConcurrent: 1
|
||||
icon: backup
|
||||
|
||||
- title: date
|
||||
shell: date
|
||||
|
||||
# This will send 1 ping (-c 1)
|
||||
# Docs: https://docs.olivetin.app/action-ping.html
|
||||
- title: Ping host
|
||||
shell: ping {{ host }} -c {{ count }}
|
||||
icon: ping
|
||||
timeout: 100
|
||||
arguments:
|
||||
- name: host
|
||||
title: host
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ type Action struct {
|
|||
Acls []string
|
||||
ExecOnStartup bool
|
||||
ExecOnCron []string
|
||||
MaxConcurrent int
|
||||
Arguments []ActionArgument
|
||||
PopupOnStart bool
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ func (action *Action) sanitize() {
|
|||
|
||||
action.Icon = lookupHTMLIcon(action.Icon)
|
||||
|
||||
if action.MaxConcurrent < 1 {
|
||||
action.MaxConcurrent = 1
|
||||
}
|
||||
|
||||
for idx := range action.Arguments {
|
||||
action.Arguments[idx].sanitize()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
|
|
@ -43,17 +44,18 @@ type ExecutionRequest struct {
|
|||
// 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
|
||||
ExecutionStarted bool
|
||||
ExecutionCompleted bool
|
||||
DatetimeStarted string
|
||||
DatetimeFinished string
|
||||
Stdout string
|
||||
Stderr string
|
||||
StdoutBuffer io.ReadCloser
|
||||
StderrBuffer io.ReadCloser
|
||||
TimedOut bool
|
||||
Blocked bool
|
||||
ExitCode int32
|
||||
Tags []string
|
||||
ExecutionStarted bool
|
||||
ExecutionFinished bool
|
||||
|
||||
/*
|
||||
The following 3 properties are obviously on Action normally, but it's useful
|
||||
|
|
@ -76,11 +78,11 @@ func DefaultExecutor() *Executor {
|
|||
e.chainOfCommand = []executorStepFunc{
|
||||
stepLogRequested,
|
||||
stepFindAction,
|
||||
stepConcurrencyCheck,
|
||||
stepACLCheck,
|
||||
stepParseArgs,
|
||||
stepLogStart,
|
||||
stepExec,
|
||||
stepNotifyListeners,
|
||||
stepLogFinish,
|
||||
}
|
||||
|
||||
|
|
@ -103,14 +105,14 @@ func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
|
|||
// 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.
|
||||
ExecutionStarted: false,
|
||||
ExecutionCompleted: false,
|
||||
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.
|
||||
ExecutionStarted: false,
|
||||
ExecutionFinished: false,
|
||||
}
|
||||
|
||||
e.Logs[req.UUID] = req.logEntry
|
||||
|
|
@ -132,6 +134,41 @@ func (e *Executor) execChain(req *ExecutionRequest) {
|
|||
break
|
||||
}
|
||||
}
|
||||
|
||||
req.logEntry.ExecutionFinished = true
|
||||
|
||||
// This isn't a step, because we want to notify all listeners, irrespective
|
||||
// of how many steps were actually executed.
|
||||
notifyListeners(req)
|
||||
}
|
||||
|
||||
func getConcurrentCount(req *ExecutionRequest) int {
|
||||
concurrentCount := 0
|
||||
|
||||
for _, log := range req.executor.Logs {
|
||||
if log.ActionTitle == req.ActionName && !log.ExecutionFinished {
|
||||
concurrentCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
return concurrentCount
|
||||
}
|
||||
|
||||
func stepConcurrencyCheck(req *ExecutionRequest) bool {
|
||||
concurrentCount := getConcurrentCount(req)
|
||||
|
||||
// Note that the current execution is counted int the logs, so when checking we +1
|
||||
if concurrentCount >= (req.action.MaxConcurrent + 1) {
|
||||
msg := fmt.Sprintf("Blocked from executing. This would mean this action is running %d times concurrently, but this action has maxExecutions set to %d.", concurrentCount, req.action.MaxConcurrent)
|
||||
|
||||
log.Warnf(msg)
|
||||
|
||||
req.logEntry.Stdout = msg
|
||||
req.logEntry.Blocked = true
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func stepFindAction(req *ExecutionRequest) bool {
|
||||
|
|
@ -202,12 +239,10 @@ func stepLogFinish(req *ExecutionRequest) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func stepNotifyListeners(req *ExecutionRequest) bool {
|
||||
func notifyListeners(req *ExecutionRequest) {
|
||||
for _, listener := range req.executor.listeners {
|
||||
listener.OnExecutionFinished(req.logEntry)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cmd {
|
||||
|
|
@ -240,7 +275,6 @@ func stepExec(req *ExecutionRequest) bool {
|
|||
// 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()
|
||||
|
|
|
|||
|
|
@ -55,16 +55,19 @@ func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *pb.ExecutionStatus
|
|||
}
|
||||
|
||||
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,
|
||||
ActionTitle: logEntry.ActionTitle,
|
||||
ActionIcon: logEntry.ActionIcon,
|
||||
DatetimeStarted: logEntry.DatetimeStarted,
|
||||
DatetimeFinished: logEntry.DatetimeFinished,
|
||||
Stdout: logEntry.Stdout,
|
||||
Stderr: logEntry.Stderr,
|
||||
TimedOut: logEntry.TimedOut,
|
||||
Blocked: logEntry.Blocked,
|
||||
ExitCode: logEntry.ExitCode,
|
||||
Tags: logEntry.Tags,
|
||||
ExecutionUuid: logEntry.UUID,
|
||||
ExecutionStarted: logEntry.ExecutionStarted,
|
||||
ExecutionFinished: logEntry.ExecutionFinished,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
|
|
@ -119,16 +122,19 @@ func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *pb.GetLogsRequest) (*pb.Ge
|
|||
|
||||
for uuid, logEntry := range api.executor.Logs {
|
||||
ret.Logs = append(ret.Logs, &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: uuid,
|
||||
ActionTitle: logEntry.ActionTitle,
|
||||
ActionIcon: logEntry.ActionIcon,
|
||||
DatetimeStarted: logEntry.DatetimeStarted,
|
||||
DatetimeFinished: logEntry.DatetimeFinished,
|
||||
Stdout: logEntry.Stdout,
|
||||
Stderr: logEntry.Stderr,
|
||||
TimedOut: logEntry.TimedOut,
|
||||
Blocked: logEntry.Blocked,
|
||||
ExitCode: logEntry.ExitCode,
|
||||
Tags: logEntry.Tags,
|
||||
ExecutionUuid: uuid,
|
||||
ExecutionStarted: logEntry.ExecutionStarted,
|
||||
ExecutionFinished: logEntry.ExecutionFinished,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -136,7 +142,7 @@ func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *pb.GetLogsRequest) (*pb.Ge
|
|||
return ret.Logs[i].DatetimeStarted < ret.Logs[j].DatetimeStarted
|
||||
}
|
||||
|
||||
sort.Slice(ret.Logs, sorter);
|
||||
sort.Slice(ret.Logs, sorter)
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,16 +37,19 @@ func (WebsocketExecutionListener) OnExecutionStarted(title string) {
|
|||
|
||||
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,
|
||||
ActionTitle: logEntry.ActionTitle,
|
||||
ActionIcon: logEntry.ActionIcon,
|
||||
DatetimeStarted: logEntry.DatetimeStarted,
|
||||
DatetimeFinished: logEntry.DatetimeFinished,
|
||||
Stdout: logEntry.Stdout,
|
||||
Stderr: logEntry.Stderr,
|
||||
TimedOut: logEntry.TimedOut,
|
||||
Blocked: logEntry.Blocked,
|
||||
ExitCode: logEntry.ExitCode,
|
||||
Tags: logEntry.Tags,
|
||||
Uuid: logEntry.UUID,
|
||||
ExecutionStarted: logEntry.ExecutionStarted,
|
||||
ExecutionFinished: logEntry.ExecutionFinished,
|
||||
}
|
||||
|
||||
broadcast("ExecutionFinished", le)
|
||||
|
|
|
|||
|
|
@ -83,12 +83,16 @@
|
|||
</h2>
|
||||
</div>
|
||||
<p>
|
||||
<strong>Started: </strong><span class = "datetimeStarted">unknown</span>
|
||||
<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>
|
||||
<p>
|
||||
<strong>Status: </strong><span class = "status">unknown</span>
|
||||
</p>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>stdout</summary>
|
||||
|
|
|
|||
|
|
@ -51,8 +51,8 @@ class ExecutionButton extends window.HTMLElement {
|
|||
onFinished (LogEntry) {
|
||||
if (LogEntry.timedOut) {
|
||||
this.onActionResult('action-timeout', 'Timed out')
|
||||
} else if (LogEntry.exitCode === -1337) {
|
||||
this.onActionError('Error')
|
||||
} else if (LogEntry.blocked) {
|
||||
this.onActionResult('action-blocked', 'Blocked!')
|
||||
} else if (LogEntry.exitCode !== 0) {
|
||||
this.onActionResult('action-nonzero-exit', 'Exit code ' + LogEntry.exitCode)
|
||||
} else {
|
||||
|
|
@ -61,7 +61,6 @@ class ExecutionButton extends window.HTMLElement {
|
|||
}
|
||||
|
||||
onActionResult (cssClass, temporaryStatusMessage) {
|
||||
this.btn.disabled = false
|
||||
this.temporaryStatusMessage = '[ ' + temporaryStatusMessage + ' ]'
|
||||
this.updateDom()
|
||||
this.btn.classList.add(cssClass)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export class ExecutionDialog {
|
|||
this.domDatetimeStarted = this.dlg.querySelector('.datetimeStarted')
|
||||
this.domDatetimeFinished = this.dlg.querySelector('.datetimeFinished')
|
||||
this.domExitCode = this.dlg.querySelector('.exitCode')
|
||||
this.domStatus = this.dlg.querySelector('.status')
|
||||
}
|
||||
|
||||
show () {
|
||||
|
|
@ -23,17 +24,24 @@ export class ExecutionDialog {
|
|||
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.executionFinished) {
|
||||
this.domStatus.innerText = 'Completed'
|
||||
this.domDatetimeFinished.innerText = res.logEntry.datetimeFinished
|
||||
|
||||
if (res.logEntry.blocked) {
|
||||
this.domStatus.innerText = 'Blocked'
|
||||
}
|
||||
|
||||
if (res.logEntry.timedOut) {
|
||||
this.domExitCode.innerText = 'Timed out'
|
||||
this.domStatus.innerText = 'Timed out'
|
||||
} else {
|
||||
this.domExitCode.innerText = res.logEntry.exitCode
|
||||
}
|
||||
|
||||
this.domDatetimeFinished.innerText = res.logEntry.datetimeFinished
|
||||
} else {
|
||||
this.domDatetimeFinished.innerText = 'Still running...'
|
||||
this.domExitCode.innerText = 'Still running...'
|
||||
this.domStatus.innerText = 'Still running...'
|
||||
}
|
||||
|
||||
this.domIcon.innerHTML = res.logEntry.actionIcon
|
||||
|
|
|
|||
|
|
@ -270,6 +270,14 @@ button.active-section {
|
|||
20% { background-color: cyan; }
|
||||
}
|
||||
|
||||
.action-blocked {
|
||||
animation: kf-action-blocked 1s;
|
||||
}
|
||||
|
||||
@keyframes kf-action-blocked {
|
||||
20% { background-color: purple; }
|
||||
}
|
||||
|
||||
footer,
|
||||
footer a {
|
||||
color: black;
|
||||
|
|
|
|||
Loading…
Reference in New Issue