feature: #146 Support for maxConcurrent (#156)

This commit is contained in:
James Read 2023-08-25 16:30:58 +01:00 committed by GitHub
parent e5a870ed94
commit e8cb661938
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 141 additions and 65 deletions

1
.gitignore vendored
View File

@ -9,3 +9,4 @@ reports
releases/
dist/
installation-id.txt
tmp/

View File

@ -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 {

View File

@ -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

View File

@ -12,6 +12,7 @@ type Action struct {
Acls []string
ExecOnStartup bool
ExecOnCron []string
MaxConcurrent int
Arguments []ActionArgument
PopupOnStart bool
}

View File

@ -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()
}

View File

@ -8,6 +8,7 @@ import (
"bytes"
"context"
"fmt"
"io"
"os/exec"
"runtime"
@ -50,10 +51,11 @@ type InternalLogEntry struct {
StdoutBuffer io.ReadCloser
StderrBuffer io.ReadCloser
TimedOut bool
Blocked bool
ExitCode int32
Tags []string
ExecutionStarted bool
ExecutionCompleted 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,
}
@ -110,7 +112,7 @@ func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
Stderr: "",
ExitCode: -1337, // If an Action is not actually executed, this is the default exit code.
ExecutionStarted: false,
ExecutionCompleted: 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()

View File

@ -62,9 +62,12 @@ func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *pb.ExecutionStatus
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
@ -126,9 +129,12 @@ func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *pb.GetLogsRequest) (*pb.Ge
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
}

View File

@ -44,9 +44,12 @@ func (WebsocketExecutionListener) OnExecutionFinished(logEntry *executor.Interna
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)

View File

@ -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>

View File

@ -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)

View File

@ -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

View File

@ -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;