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/ releases/
dist/ dist/
installation-id.txt installation-id.txt
tmp/

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@ import (
"bytes" "bytes"
"context" "context"
"fmt"
"io" "io"
"os/exec" "os/exec"
"runtime" "runtime"
@ -50,10 +51,11 @@ type InternalLogEntry struct {
StdoutBuffer io.ReadCloser StdoutBuffer io.ReadCloser
StderrBuffer io.ReadCloser StderrBuffer io.ReadCloser
TimedOut bool TimedOut bool
Blocked bool
ExitCode int32 ExitCode int32
Tags []string Tags []string
ExecutionStarted bool ExecutionStarted bool
ExecutionCompleted 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,
} }
@ -110,7 +112,7 @@ func (e *Executor) ExecRequest(req *ExecutionRequest) *pb.StartActionResponse {
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()

View File

@ -62,9 +62,12 @@ func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *pb.ExecutionStatus
Stdout: logEntry.Stdout, Stdout: logEntry.Stdout,
Stderr: logEntry.Stderr, Stderr: logEntry.Stderr,
TimedOut: logEntry.TimedOut, TimedOut: logEntry.TimedOut,
Blocked: logEntry.Blocked,
ExitCode: logEntry.ExitCode, ExitCode: logEntry.ExitCode,
Tags: logEntry.Tags, Tags: logEntry.Tags,
ExecutionUuid: logEntry.UUID, ExecutionUuid: logEntry.UUID,
ExecutionStarted: logEntry.ExecutionStarted,
ExecutionFinished: logEntry.ExecutionFinished,
} }
return res, nil return res, nil
@ -126,9 +129,12 @@ func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *pb.GetLogsRequest) (*pb.Ge
Stdout: logEntry.Stdout, Stdout: logEntry.Stdout,
Stderr: logEntry.Stderr, Stderr: logEntry.Stderr,
TimedOut: logEntry.TimedOut, TimedOut: logEntry.TimedOut,
Blocked: logEntry.Blocked,
ExitCode: logEntry.ExitCode, ExitCode: logEntry.ExitCode,
Tags: logEntry.Tags, Tags: logEntry.Tags,
ExecutionUuid: uuid, 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
} }

View File

@ -44,9 +44,12 @@ func (WebsocketExecutionListener) OnExecutionFinished(logEntry *executor.Interna
Stdout: logEntry.Stdout, Stdout: logEntry.Stdout,
Stderr: logEntry.Stderr, Stderr: logEntry.Stderr,
TimedOut: logEntry.TimedOut, TimedOut: logEntry.TimedOut,
Blocked: logEntry.Blocked,
ExitCode: logEntry.ExitCode, ExitCode: logEntry.ExitCode,
Tags: logEntry.Tags, Tags: logEntry.Tags,
Uuid: logEntry.UUID, Uuid: logEntry.UUID,
ExecutionStarted: logEntry.ExecutionStarted,
ExecutionFinished: logEntry.ExecutionFinished,
} }
broadcast("ExecutionFinished", le) broadcast("ExecutionFinished", le)

View File

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

View File

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

View File

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

View File

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