feature: Additional APIs for StartAction (#188)

This commit is contained in:
James Read 2023-10-31 18:17:35 +00:00 committed by GitHub
parent ca921c1890
commit d0f74c1ab7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 188 additions and 39 deletions

View File

@ -60,6 +60,30 @@ message StartActionResponse {
string execution_uuid = 2; string execution_uuid = 2;
} }
message StartActionAndWaitRequest {
string action_name = 1;
}
message StartActionAndWaitResponse {
LogEntry log_entry = 1;
}
message StartActionByAliasRequest {
string action_alias = 1;
}
message StartActionByAliasResponse {
string execution_uuid = 2;
}
message StartActionByAliasAndWaitRequest {
string action_alias = 1;
}
message StartActionByAliasAndWaitResponse {
LogEntry log_entry = 1;
}
message GetLogsRequest{}; message GetLogsRequest{};
message LogEntry { message LogEntry {
@ -137,6 +161,27 @@ service OliveTinApiService {
}; };
} }
rpc StartActionAndWait(StartActionAndWaitRequest) returns (StartActionAndWaitResponse) {
option (google.api.http) = {
post: "/api/StartActionAndWait"
body: "*"
};
}
rpc StartActionByAlias(StartActionByAliasRequest) returns (StartActionByAliasResponse) {
option (google.api.http) = {
post: "/api/StartActionByAlias/{action_alias}"
body: "*"
};
}
rpc StartActionByAliasAndWait(StartActionByAliasAndWaitRequest) returns (StartActionByAliasAndWaitResponse) {
option (google.api.http) = {
post: "/api/StartActionByAliasAndWait/{action_alias}"
body: "*"
};
}
rpc ExecutionStatus(ExecutionStatusRequest) returns (ExecutionStatusResponse) { rpc ExecutionStatus(ExecutionStatusRequest) returns (ExecutionStatusResponse) {
option (google.api.http) = { option (google.api.http) = {
post: "/api/ExecutionStatus" post: "/api/ExecutionStatus"

View File

@ -43,6 +43,7 @@ actions:
# Restart lightdm on host "server1" # Restart lightdm on host "server1"
# Docs: https://docs.olivetin.app/action-ping.html # Docs: https://docs.olivetin.app/action-ping.html
- title: restart httpd - title: restart httpd
titleAlias: restart_httpd
icon: restart icon: restart
shell: ssh root@server1 'service httpd restart' shell: ssh root@server1 'service httpd restart'

View File

@ -5,6 +5,7 @@ package config
type Action struct { type Action struct {
ID string ID string
Title string Title string
TitleAlias string
Icon string Icon string
Shell string Shell string
ShellAfterCompleted string ShellAfterCompleted string

View File

@ -29,10 +29,10 @@ type Executor struct {
// Executor. They're created from the grpcapi. // Executor. They're created from the grpcapi.
type ExecutionRequest struct { type ExecutionRequest struct {
ActionName string ActionName string
Action *config.Action
Arguments map[string]string Arguments map[string]string
UUID string UUID string
Tags []string Tags []string
action *config.Action
Cfg *config.Config Cfg *config.Config
AuthenticatedUser *acl.AuthenticatedUser AuthenticatedUser *acl.AuthenticatedUser
logEntry *InternalLogEntry logEntry *InternalLogEntry
@ -165,8 +165,8 @@ func stepConcurrencyCheck(req *ExecutionRequest) bool {
concurrentCount := getConcurrentCount(req) concurrentCount := getConcurrentCount(req)
// Note that the current execution is counted int the logs, so when checking we +1 // Note that the current execution is counted int the logs, so when checking we +1
if concurrentCount >= (req.action.MaxConcurrent + 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) 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.WithFields(log.Fields{ log.WithFields(log.Fields{
"actionTitle": req.ActionName, "actionTitle": req.ActionName,
@ -181,6 +181,10 @@ func stepConcurrencyCheck(req *ExecutionRequest) bool {
} }
func stepFindAction(req *ExecutionRequest) bool { func stepFindAction(req *ExecutionRequest) bool {
if req.Action != nil {
return true
}
actualAction := req.Cfg.FindAction(req.ActionName) actualAction := req.Cfg.FindAction(req.ActionName)
if actualAction == nil { if actualAction == nil {
@ -193,20 +197,20 @@ func stepFindAction(req *ExecutionRequest) bool {
return false return false
} }
req.action = actualAction req.Action = actualAction
req.logEntry.ActionIcon = actualAction.Icon req.logEntry.ActionIcon = actualAction.Icon
return true return true
} }
func stepACLCheck(req *ExecutionRequest) bool { func stepACLCheck(req *ExecutionRequest) bool {
return acl.IsAllowedExec(req.Cfg, req.AuthenticatedUser, req.action) return acl.IsAllowedExec(req.Cfg, req.AuthenticatedUser, req.Action)
} }
func stepParseArgs(req *ExecutionRequest) bool { func stepParseArgs(req *ExecutionRequest) bool {
var err error var err error
req.finalParsedCommand, err = parseActionArguments(req.action.Shell, req.Arguments, req.action) req.finalParsedCommand, err = parseActionArguments(req.Action.Shell, req.Arguments, req.Action)
if err != nil { if err != nil {
req.logEntry.Stdout = err.Error() req.logEntry.Stdout = err.Error()
@ -229,8 +233,8 @@ func stepLogRequested(req *ExecutionRequest) bool {
func stepLogStart(req *ExecutionRequest) bool { func stepLogStart(req *ExecutionRequest) bool {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"actionTitle": req.action.Title, "actionTitle": req.Action.Title,
"timeout": req.action.Timeout, "timeout": req.Action.Timeout,
}).Infof("Action starting") }).Infof("Action starting")
return true return true
@ -238,7 +242,7 @@ func stepLogStart(req *ExecutionRequest) bool {
func stepLogFinish(req *ExecutionRequest) bool { func stepLogFinish(req *ExecutionRequest) bool {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"actionTitle": req.action.Title, "actionTitle": req.Action.Title,
"stdout": req.logEntry.Stdout, "stdout": req.logEntry.Stdout,
"stderr": req.logEntry.Stderr, "stderr": req.logEntry.Stderr,
"timedOut": req.logEntry.TimedOut, "timedOut": req.logEntry.TimedOut,
@ -263,7 +267,7 @@ func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cm
} }
func stepExec(req *ExecutionRequest) bool { func stepExec(req *ExecutionRequest) bool {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.action.Timeout)*time.Second) ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.Action.Timeout)*time.Second)
defer cancel() defer cancel()
var stdout bytes.Buffer var stdout bytes.Buffer
@ -303,11 +307,11 @@ func stepExec(req *ExecutionRequest) bool {
} }
func stepExecAfter(req *ExecutionRequest) bool { func stepExecAfter(req *ExecutionRequest) bool {
if req.action.ShellAfterCompleted == "" { if req.Action.ShellAfterCompleted == "" {
return true return true
} }
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.action.Timeout)*time.Second) ctx, cancel := context.WithTimeout(context.Background(), time.Duration(req.Action.Timeout)*time.Second)
defer cancel() defer cancel()
var stdout bytes.Buffer var stdout bytes.Buffer
@ -318,7 +322,7 @@ func stepExecAfter(req *ExecutionRequest) bool {
"exitCode": fmt.Sprintf("%v", req.logEntry.ExitCode), "exitCode": fmt.Sprintf("%v", req.logEntry.ExitCode),
} }
finalParsedCommand, _ := parseActionArguments(req.action.ShellAfterCompleted, args, req.action) finalParsedCommand, _ := parseActionArguments(req.Action.ShellAfterCompleted, args, req.Action)
cmd := wrapCommandInShell(ctx, finalParsedCommand) cmd := wrapCommandInShell(ctx, finalParsedCommand)
cmd.Stdout = &stdout cmd.Stdout = &stdout

View File

@ -3,9 +3,11 @@ package grpcapi
import ( import (
ctx "context" ctx "context"
pb "github.com/OliveTin/OliveTin/gen/grpc" pb "github.com/OliveTin/OliveTin/gen/grpc"
"github.com/google/uuid"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"google.golang.org/grpc" "google.golang.org/grpc"
"errors"
"net" "net"
"sort" "sort"
@ -20,7 +22,8 @@ var (
) )
type oliveTinAPI struct { type oliveTinAPI struct {
pb.UnimplementedOliveTinApiServiceServer // Uncomment this if you want to allow undefined methods during dev.
// pb.UnimplementedOliveTinApiServiceServer
executor *executor.Executor executor *executor.Executor
} }
@ -28,8 +31,6 @@ type oliveTinAPI struct {
func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *pb.StartActionRequest) (*pb.StartActionResponse, error) { func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *pb.StartActionRequest) (*pb.StartActionResponse, error) {
args := make(map[string]string) args := make(map[string]string)
log.Debugf("SA %v", req)
for _, arg := range req.Arguments { for _, arg := range req.Arguments {
args[arg.Name] = arg.Value args[arg.Name] = arg.Value
} }
@ -47,19 +48,103 @@ func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *pb.StartActionRequest)
return &pb.StartActionResponse{ return &pb.StartActionResponse{
ExecutionUuid: uuid, ExecutionUuid: uuid,
}, nil }, nil
} }
func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *pb.ExecutionStatusRequest) (*pb.ExecutionStatusResponse, error) { func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *pb.StartActionAndWaitRequest) (*pb.StartActionAndWaitResponse, error) {
res := &pb.ExecutionStatusResponse{} args := make(map[string]string)
logEntry, ok := api.executor.Logs[req.ExecutionUuid] execReq := executor.ExecutionRequest{
ActionName: req.ActionName,
if !ok { UUID: uuid.NewString(),
return res, nil Arguments: args,
AuthenticatedUser: acl.UserFromContext(ctx, cfg),
Cfg: cfg,
} }
res.LogEntry = &pb.LogEntry{ wg, _ := api.executor.ExecRequest(&execReq)
wg.Wait()
internalLogEntry, ok := api.executor.Logs[execReq.UUID]
if ok {
return &pb.StartActionAndWaitResponse{
LogEntry: internalLogEntryToPb(internalLogEntry),
}, nil
} else {
return nil, errors.New("Execution not found!")
}
}
func (api *oliveTinAPI) StartActionByAlias(ctx ctx.Context, req *pb.StartActionByAliasRequest) (*pb.StartActionByAliasResponse, error) {
args := make(map[string]string)
action := findActionByAlias(req.ActionAlias)
if action == nil {
log.Warnf("ByAlias action alias not found: %v, cannot start execution.", req.ActionAlias)
return &pb.StartActionByAliasResponse{
ExecutionUuid: "",
}, errors.New("ByAlias action alias not found")
}
execReq := executor.ExecutionRequest{
ActionName: action.Title,
Action: action,
UUID: uuid.NewString(),
Arguments: args,
AuthenticatedUser: &acl.AuthenticatedUser{
Username: "webhook",
Usergroup: "webhook",
},
Cfg: cfg,
}
_, uuid := api.executor.ExecRequest(&execReq)
return &pb.StartActionByAliasResponse{
ExecutionUuid: uuid,
}, nil
}
func (api *oliveTinAPI) StartActionByAliasAndWait(ctx ctx.Context, req *pb.StartActionByAliasAndWaitRequest) (*pb.StartActionByAliasAndWaitResponse, error) {
args := make(map[string]string)
action := findActionByAlias(req.ActionAlias)
if action == nil {
log.Warnf("ByAlias action alias not found: %v, cannot start execution.", req.ActionAlias)
return &pb.StartActionByAliasAndWaitResponse{}, errors.New("ByAlias action alias not found")
}
execReq := executor.ExecutionRequest{
ActionName: action.Title,
Action: action,
UUID: uuid.NewString(),
Arguments: args,
AuthenticatedUser: &acl.AuthenticatedUser{
Username: "webhook",
Usergroup: "webhook",
},
Cfg: cfg,
}
wg, _ := api.executor.ExecRequest(&execReq)
wg.Wait()
internalLogEntry, ok := api.executor.Logs[execReq.UUID]
if ok {
return &pb.StartActionByAliasAndWaitResponse{
LogEntry: internalLogEntryToPb(internalLogEntry),
}, nil
} else {
return nil, errors.New("Execution not found!")
}
}
func internalLogEntryToPb(logEntry *executor.InternalLogEntry) *pb.LogEntry {
return &pb.LogEntry{
ActionTitle: logEntry.ActionTitle, ActionTitle: logEntry.ActionTitle,
ActionIcon: logEntry.ActionIcon, ActionIcon: logEntry.ActionIcon,
DatetimeStarted: logEntry.DatetimeStarted, DatetimeStarted: logEntry.DatetimeStarted,
@ -74,6 +159,18 @@ func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *pb.ExecutionStatus
ExecutionStarted: logEntry.ExecutionStarted, ExecutionStarted: logEntry.ExecutionStarted,
ExecutionFinished: logEntry.ExecutionFinished, ExecutionFinished: logEntry.ExecutionFinished,
} }
}
func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *pb.ExecutionStatusRequest) (*pb.ExecutionStatusResponse, error) {
res := &pb.ExecutionStatusResponse{}
logEntry, ok := api.executor.Logs[req.ExecutionUuid]
if !ok {
return res, nil
}
res.LogEntry = internalLogEntryToPb(logEntry)
return res, nil return res, nil
} }
@ -126,21 +223,10 @@ func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *pb.GetLogsRequest) (*pb.Ge
// TODO Limit to 10 entries or something to prevent browser lag. // TODO Limit to 10 entries or something to prevent browser lag.
for uuid, logEntry := range api.executor.Logs { for uuid, logEntry := range api.executor.Logs {
ret.Logs = append(ret.Logs, &pb.LogEntry{ pbLogEntry := internalLogEntryToPb(logEntry)
ActionTitle: logEntry.ActionTitle, pbLogEntry.ExecutionUuid = uuid
ActionIcon: logEntry.ActionIcon,
DatetimeStarted: logEntry.DatetimeStarted, ret.Logs = append(ret.Logs, pbLogEntry)
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,
})
} }
sorter := func(i, j int) bool { sorter := func(i, j int) bool {

View File

@ -62,3 +62,15 @@ func buildChoices(choices []config.ActionArgumentChoice) []*pb.ActionArgumentCho
return ret return ret
} }
func findActionByAlias(alias string) *config.Action {
for _, action := range cfg.Actions {
if action.TitleAlias != "" {
if action.TitleAlias == alias {
return &action
}
}
}
return nil
}