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

View File

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

View File

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

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;