parent
e5a870ed94
commit
e8cb661938
|
|
@ -9,3 +9,4 @@ reports
|
||||||
releases/
|
releases/
|
||||||
dist/
|
dist/
|
||||||
installation-id.txt
|
installation-id.txt
|
||||||
|
tmp/
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,9 @@ message LogEntry {
|
||||||
string execution_uuid = 11;
|
string execution_uuid = 11;
|
||||||
string datetime_finished = 12;
|
string datetime_finished = 12;
|
||||||
string uuid = 13;
|
string uuid = 13;
|
||||||
|
bool execution_started = 14;
|
||||||
|
bool execution_finished = 15;
|
||||||
|
bool blocked = 16;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetLogsResponse {
|
message GetLogsResponse {
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,18 @@ actions:
|
||||||
# This will run a simple script that you create.
|
# This will run a simple script that you create.
|
||||||
- title: Run backup script
|
- title: Run backup script
|
||||||
shell: /opt/backupScript.sh
|
shell: /opt/backupScript.sh
|
||||||
|
maxConcurrent: 1
|
||||||
icon: backup
|
icon: backup
|
||||||
|
|
||||||
|
- title: date
|
||||||
|
shell: date
|
||||||
|
|
||||||
# This will send 1 ping (-c 1)
|
# This will send 1 ping (-c 1)
|
||||||
# Docs: https://docs.olivetin.app/action-ping.html
|
# Docs: https://docs.olivetin.app/action-ping.html
|
||||||
- title: Ping host
|
- title: Ping host
|
||||||
shell: ping {{ host }} -c {{ count }}
|
shell: ping {{ host }} -c {{ count }}
|
||||||
icon: ping
|
icon: ping
|
||||||
|
timeout: 100
|
||||||
arguments:
|
arguments:
|
||||||
- name: host
|
- name: host
|
||||||
title: host
|
title: host
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ type Action struct {
|
||||||
Acls []string
|
Acls []string
|
||||||
ExecOnStartup bool
|
ExecOnStartup bool
|
||||||
ExecOnCron []string
|
ExecOnCron []string
|
||||||
|
MaxConcurrent int
|
||||||
Arguments []ActionArgument
|
Arguments []ActionArgument
|
||||||
PopupOnStart bool
|
PopupOnStart bool
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,10 @@ func (action *Action) sanitize() {
|
||||||
|
|
||||||
action.Icon = lookupHTMLIcon(action.Icon)
|
action.Icon = lookupHTMLIcon(action.Icon)
|
||||||
|
|
||||||
|
if action.MaxConcurrent < 1 {
|
||||||
|
action.MaxConcurrent = 1
|
||||||
|
}
|
||||||
|
|
||||||
for idx := range action.Arguments {
|
for idx := range action.Arguments {
|
||||||
action.Arguments[idx].sanitize()
|
action.Arguments[idx].sanitize()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
@ -43,17 +44,18 @@ type ExecutionRequest struct {
|
||||||
// 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 {
|
||||||
DatetimeStarted string
|
DatetimeStarted string
|
||||||
DatetimeFinished string
|
DatetimeFinished string
|
||||||
Stdout string
|
Stdout string
|
||||||
Stderr string
|
Stderr string
|
||||||
StdoutBuffer io.ReadCloser
|
StdoutBuffer io.ReadCloser
|
||||||
StderrBuffer io.ReadCloser
|
StderrBuffer io.ReadCloser
|
||||||
TimedOut bool
|
TimedOut bool
|
||||||
ExitCode int32
|
Blocked bool
|
||||||
Tags []string
|
ExitCode int32
|
||||||
ExecutionStarted bool
|
Tags []string
|
||||||
ExecutionCompleted bool
|
ExecutionStarted bool
|
||||||
|
ExecutionFinished bool
|
||||||
|
|
||||||
/*
|
/*
|
||||||
The following 3 properties are obviously on Action normally, but it's useful
|
The following 3 properties are obviously on Action normally, but it's useful
|
||||||
|
|
@ -76,11 +78,11 @@ func DefaultExecutor() *Executor {
|
||||||
e.chainOfCommand = []executorStepFunc{
|
e.chainOfCommand = []executorStepFunc{
|
||||||
stepLogRequested,
|
stepLogRequested,
|
||||||
stepFindAction,
|
stepFindAction,
|
||||||
|
stepConcurrencyCheck,
|
||||||
stepACLCheck,
|
stepACLCheck,
|
||||||
stepParseArgs,
|
stepParseArgs,
|
||||||
stepLogStart,
|
stepLogStart,
|
||||||
stepExec,
|
stepExec,
|
||||||
stepNotifyListeners,
|
|
||||||
stepLogFinish,
|
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.
|
// duplicate UUIDs (or just random strings), but this is the only way.
|
||||||
req.executor = e
|
req.executor = e
|
||||||
req.logEntry = &InternalLogEntry{
|
req.logEntry = &InternalLogEntry{
|
||||||
DatetimeStarted: time.Now().Format("2006-01-02 15:04:05"),
|
DatetimeStarted: time.Now().Format("2006-01-02 15:04:05"),
|
||||||
ActionTitle: req.ActionName,
|
ActionTitle: req.ActionName,
|
||||||
UUID: req.UUID,
|
UUID: req.UUID,
|
||||||
Stdout: "",
|
Stdout: "",
|
||||||
Stderr: "",
|
Stderr: "",
|
||||||
ExitCode: -1337, // If an Action is not actually executed, this is the default exit code.
|
ExitCode: -1337, // If an Action is not actually executed, this is the default exit code.
|
||||||
ExecutionStarted: false,
|
ExecutionStarted: false,
|
||||||
ExecutionCompleted: false,
|
ExecutionFinished: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
e.Logs[req.UUID] = req.logEntry
|
e.Logs[req.UUID] = req.logEntry
|
||||||
|
|
@ -132,6 +134,41 @@ func (e *Executor) execChain(req *ExecutionRequest) {
|
||||||
break
|
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 {
|
func stepFindAction(req *ExecutionRequest) bool {
|
||||||
|
|
@ -202,12 +239,10 @@ func stepLogFinish(req *ExecutionRequest) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func stepNotifyListeners(req *ExecutionRequest) bool {
|
func notifyListeners(req *ExecutionRequest) {
|
||||||
for _, listener := range req.executor.listeners {
|
for _, listener := range req.executor.listeners {
|
||||||
listener.OnExecutionFinished(req.logEntry)
|
listener.OnExecutionFinished(req.logEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cmd {
|
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.Stdout = req.logEntry.StdoutBuffer.String()
|
||||||
// req.logEntry.Stderr = req.logEntry.StderrBuffer.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()
|
||||||
|
|
|
||||||
|
|
@ -55,16 +55,19 @@ func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *pb.ExecutionStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
res.LogEntry = &pb.LogEntry{
|
res.LogEntry = &pb.LogEntry{
|
||||||
ActionTitle: logEntry.ActionTitle,
|
ActionTitle: logEntry.ActionTitle,
|
||||||
ActionIcon: logEntry.ActionIcon,
|
ActionIcon: logEntry.ActionIcon,
|
||||||
DatetimeStarted: logEntry.DatetimeStarted,
|
DatetimeStarted: logEntry.DatetimeStarted,
|
||||||
DatetimeFinished: logEntry.DatetimeFinished,
|
DatetimeFinished: logEntry.DatetimeFinished,
|
||||||
Stdout: logEntry.Stdout,
|
Stdout: logEntry.Stdout,
|
||||||
Stderr: logEntry.Stderr,
|
Stderr: logEntry.Stderr,
|
||||||
TimedOut: logEntry.TimedOut,
|
TimedOut: logEntry.TimedOut,
|
||||||
ExitCode: logEntry.ExitCode,
|
Blocked: logEntry.Blocked,
|
||||||
Tags: logEntry.Tags,
|
ExitCode: logEntry.ExitCode,
|
||||||
ExecutionUuid: logEntry.UUID,
|
Tags: logEntry.Tags,
|
||||||
|
ExecutionUuid: logEntry.UUID,
|
||||||
|
ExecutionStarted: logEntry.ExecutionStarted,
|
||||||
|
ExecutionFinished: logEntry.ExecutionFinished,
|
||||||
}
|
}
|
||||||
|
|
||||||
return res, nil
|
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 {
|
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,
|
||||||
DatetimeStarted: logEntry.DatetimeStarted,
|
DatetimeStarted: logEntry.DatetimeStarted,
|
||||||
DatetimeFinished: logEntry.DatetimeFinished,
|
DatetimeFinished: logEntry.DatetimeFinished,
|
||||||
Stdout: logEntry.Stdout,
|
Stdout: logEntry.Stdout,
|
||||||
Stderr: logEntry.Stderr,
|
Stderr: logEntry.Stderr,
|
||||||
TimedOut: logEntry.TimedOut,
|
TimedOut: logEntry.TimedOut,
|
||||||
ExitCode: logEntry.ExitCode,
|
Blocked: logEntry.Blocked,
|
||||||
Tags: logEntry.Tags,
|
ExitCode: logEntry.ExitCode,
|
||||||
ExecutionUuid: uuid,
|
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
|
return ret.Logs[i].DatetimeStarted < ret.Logs[j].DatetimeStarted
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(ret.Logs, sorter);
|
sort.Slice(ret.Logs, sorter)
|
||||||
|
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,16 +37,19 @@ func (WebsocketExecutionListener) OnExecutionStarted(title string) {
|
||||||
|
|
||||||
func (WebsocketExecutionListener) OnExecutionFinished(logEntry *executor.InternalLogEntry) {
|
func (WebsocketExecutionListener) OnExecutionFinished(logEntry *executor.InternalLogEntry) {
|
||||||
le := &pb.LogEntry{
|
le := &pb.LogEntry{
|
||||||
ActionTitle: logEntry.ActionTitle,
|
ActionTitle: logEntry.ActionTitle,
|
||||||
ActionIcon: logEntry.ActionIcon,
|
ActionIcon: logEntry.ActionIcon,
|
||||||
DatetimeStarted: logEntry.DatetimeStarted,
|
DatetimeStarted: logEntry.DatetimeStarted,
|
||||||
DatetimeFinished: logEntry.DatetimeFinished,
|
DatetimeFinished: logEntry.DatetimeFinished,
|
||||||
Stdout: logEntry.Stdout,
|
Stdout: logEntry.Stdout,
|
||||||
Stderr: logEntry.Stderr,
|
Stderr: logEntry.Stderr,
|
||||||
TimedOut: logEntry.TimedOut,
|
TimedOut: logEntry.TimedOut,
|
||||||
ExitCode: logEntry.ExitCode,
|
Blocked: logEntry.Blocked,
|
||||||
Tags: logEntry.Tags,
|
ExitCode: logEntry.ExitCode,
|
||||||
Uuid: logEntry.UUID,
|
Tags: logEntry.Tags,
|
||||||
|
Uuid: logEntry.UUID,
|
||||||
|
ExecutionStarted: logEntry.ExecutionStarted,
|
||||||
|
ExecutionFinished: logEntry.ExecutionFinished,
|
||||||
}
|
}
|
||||||
|
|
||||||
broadcast("ExecutionFinished", le)
|
broadcast("ExecutionFinished", le)
|
||||||
|
|
|
||||||
|
|
@ -83,12 +83,16 @@
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<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>
|
<strong>Finished: </strong><span class = "datetimeFinished">unknown</span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Exit Code: </strong><span class = "exitCode">unknown</span>
|
<strong>Exit Code: </strong><span class = "exitCode">unknown</span>
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Status: </strong><span class = "status">unknown</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>stdout</summary>
|
<summary>stdout</summary>
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,8 @@ class ExecutionButton extends window.HTMLElement {
|
||||||
onFinished (LogEntry) {
|
onFinished (LogEntry) {
|
||||||
if (LogEntry.timedOut) {
|
if (LogEntry.timedOut) {
|
||||||
this.onActionResult('action-timeout', 'Timed out')
|
this.onActionResult('action-timeout', 'Timed out')
|
||||||
} else if (LogEntry.exitCode === -1337) {
|
} else if (LogEntry.blocked) {
|
||||||
this.onActionError('Error')
|
this.onActionResult('action-blocked', 'Blocked!')
|
||||||
} else if (LogEntry.exitCode !== 0) {
|
} else if (LogEntry.exitCode !== 0) {
|
||||||
this.onActionResult('action-nonzero-exit', 'Exit code ' + LogEntry.exitCode)
|
this.onActionResult('action-nonzero-exit', 'Exit code ' + LogEntry.exitCode)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -61,7 +61,6 @@ class ExecutionButton extends window.HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
onActionResult (cssClass, temporaryStatusMessage) {
|
onActionResult (cssClass, temporaryStatusMessage) {
|
||||||
this.btn.disabled = false
|
|
||||||
this.temporaryStatusMessage = '[ ' + temporaryStatusMessage + ' ]'
|
this.temporaryStatusMessage = '[ ' + temporaryStatusMessage + ' ]'
|
||||||
this.updateDom()
|
this.updateDom()
|
||||||
this.btn.classList.add(cssClass)
|
this.btn.classList.add(cssClass)
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export class ExecutionDialog {
|
||||||
this.domDatetimeStarted = this.dlg.querySelector('.datetimeStarted')
|
this.domDatetimeStarted = this.dlg.querySelector('.datetimeStarted')
|
||||||
this.domDatetimeFinished = this.dlg.querySelector('.datetimeFinished')
|
this.domDatetimeFinished = this.dlg.querySelector('.datetimeFinished')
|
||||||
this.domExitCode = this.dlg.querySelector('.exitCode')
|
this.domExitCode = this.dlg.querySelector('.exitCode')
|
||||||
|
this.domStatus = this.dlg.querySelector('.status')
|
||||||
}
|
}
|
||||||
|
|
||||||
show () {
|
show () {
|
||||||
|
|
@ -23,17 +24,24 @@ export class ExecutionDialog {
|
||||||
renderResult (res) {
|
renderResult (res) {
|
||||||
this.executionUuid = res.logEntry.executionUuid
|
this.executionUuid = res.logEntry.executionUuid
|
||||||
|
|
||||||
if (res.logEntry.datetimeFinished === '') {
|
if (res.logEntry.executionFinished) {
|
||||||
this.domExitCode.innerText = 'Still running...'
|
this.domStatus.innerText = 'Completed'
|
||||||
this.domDatetimeFinished.innerText = 'Still running...'
|
this.domDatetimeFinished.innerText = res.logEntry.datetimeFinished
|
||||||
} else {
|
|
||||||
|
if (res.logEntry.blocked) {
|
||||||
|
this.domStatus.innerText = 'Blocked'
|
||||||
|
}
|
||||||
|
|
||||||
if (res.logEntry.timedOut) {
|
if (res.logEntry.timedOut) {
|
||||||
this.domExitCode.innerText = 'Timed out'
|
this.domExitCode.innerText = 'Timed out'
|
||||||
|
this.domStatus.innerText = 'Timed out'
|
||||||
} else {
|
} else {
|
||||||
this.domExitCode.innerText = res.logEntry.exitCode
|
this.domExitCode.innerText = res.logEntry.exitCode
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
this.domDatetimeFinished.innerText = res.logEntry.datetimeFinished
|
this.domDatetimeFinished.innerText = 'Still running...'
|
||||||
|
this.domExitCode.innerText = 'Still running...'
|
||||||
|
this.domStatus.innerText = 'Still running...'
|
||||||
}
|
}
|
||||||
|
|
||||||
this.domIcon.innerHTML = res.logEntry.actionIcon
|
this.domIcon.innerHTML = res.logEntry.actionIcon
|
||||||
|
|
|
||||||
|
|
@ -270,6 +270,14 @@ button.active-section {
|
||||||
20% { background-color: cyan; }
|
20% { background-color: cyan; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-blocked {
|
||||||
|
animation: kf-action-blocked 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes kf-action-blocked {
|
||||||
|
20% { background-color: purple; }
|
||||||
|
}
|
||||||
|
|
||||||
footer,
|
footer,
|
||||||
footer a {
|
footer a {
|
||||||
color: black;
|
color: black;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue