feature: Additional APIs for StartAction (#188)
This commit is contained in:
parent
ca921c1890
commit
d0f74c1ab7
|
|
@ -60,6 +60,30 @@ message StartActionResponse {
|
|||
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 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) {
|
||||
option (google.api.http) = {
|
||||
post: "/api/ExecutionStatus"
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ actions:
|
|||
# Restart lightdm on host "server1"
|
||||
# Docs: https://docs.olivetin.app/action-ping.html
|
||||
- title: restart httpd
|
||||
titleAlias: restart_httpd
|
||||
icon: restart
|
||||
shell: ssh root@server1 'service httpd restart'
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ package config
|
|||
type Action struct {
|
||||
ID string
|
||||
Title string
|
||||
TitleAlias string
|
||||
Icon string
|
||||
Shell string
|
||||
ShellAfterCompleted string
|
||||
|
|
|
|||
|
|
@ -29,10 +29,10 @@ type Executor struct {
|
|||
// Executor. They're created from the grpcapi.
|
||||
type ExecutionRequest struct {
|
||||
ActionName string
|
||||
Action *config.Action
|
||||
Arguments map[string]string
|
||||
UUID string
|
||||
Tags []string
|
||||
action *config.Action
|
||||
Cfg *config.Config
|
||||
AuthenticatedUser *acl.AuthenticatedUser
|
||||
logEntry *InternalLogEntry
|
||||
|
|
@ -165,8 +165,8 @@ 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)
|
||||
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.WithFields(log.Fields{
|
||||
"actionTitle": req.ActionName,
|
||||
|
|
@ -181,6 +181,10 @@ func stepConcurrencyCheck(req *ExecutionRequest) bool {
|
|||
}
|
||||
|
||||
func stepFindAction(req *ExecutionRequest) bool {
|
||||
if req.Action != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
actualAction := req.Cfg.FindAction(req.ActionName)
|
||||
|
||||
if actualAction == nil {
|
||||
|
|
@ -193,20 +197,20 @@ func stepFindAction(req *ExecutionRequest) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
req.action = actualAction
|
||||
req.Action = actualAction
|
||||
req.logEntry.ActionIcon = actualAction.Icon
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
req.logEntry.Stdout = err.Error()
|
||||
|
|
@ -229,8 +233,8 @@ func stepLogRequested(req *ExecutionRequest) bool {
|
|||
|
||||
func stepLogStart(req *ExecutionRequest) bool {
|
||||
log.WithFields(log.Fields{
|
||||
"actionTitle": req.action.Title,
|
||||
"timeout": req.action.Timeout,
|
||||
"actionTitle": req.Action.Title,
|
||||
"timeout": req.Action.Timeout,
|
||||
}).Infof("Action starting")
|
||||
|
||||
return true
|
||||
|
|
@ -238,7 +242,7 @@ func stepLogStart(req *ExecutionRequest) bool {
|
|||
|
||||
func stepLogFinish(req *ExecutionRequest) bool {
|
||||
log.WithFields(log.Fields{
|
||||
"actionTitle": req.action.Title,
|
||||
"actionTitle": req.Action.Title,
|
||||
"stdout": req.logEntry.Stdout,
|
||||
"stderr": req.logEntry.Stderr,
|
||||
"timedOut": req.logEntry.TimedOut,
|
||||
|
|
@ -263,7 +267,7 @@ func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cm
|
|||
}
|
||||
|
||||
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()
|
||||
|
||||
var stdout bytes.Buffer
|
||||
|
|
@ -303,11 +307,11 @@ func stepExec(req *ExecutionRequest) bool {
|
|||
}
|
||||
|
||||
func stepExecAfter(req *ExecutionRequest) bool {
|
||||
if req.action.ShellAfterCompleted == "" {
|
||||
if req.Action.ShellAfterCompleted == "" {
|
||||
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()
|
||||
|
||||
var stdout bytes.Buffer
|
||||
|
|
@ -318,7 +322,7 @@ func stepExecAfter(req *ExecutionRequest) bool {
|
|||
"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.Stdout = &stdout
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ package grpcapi
|
|||
import (
|
||||
ctx "context"
|
||||
pb "github.com/OliveTin/OliveTin/gen/grpc"
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"errors"
|
||||
"net"
|
||||
"sort"
|
||||
|
||||
|
|
@ -20,7 +22,8 @@ var (
|
|||
)
|
||||
|
||||
type oliveTinAPI struct {
|
||||
pb.UnimplementedOliveTinApiServiceServer
|
||||
// Uncomment this if you want to allow undefined methods during dev.
|
||||
// pb.UnimplementedOliveTinApiServiceServer
|
||||
|
||||
executor *executor.Executor
|
||||
}
|
||||
|
|
@ -28,8 +31,6 @@ type oliveTinAPI struct {
|
|||
func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *pb.StartActionRequest) (*pb.StartActionResponse, error) {
|
||||
args := make(map[string]string)
|
||||
|
||||
log.Debugf("SA %v", req)
|
||||
|
||||
for _, arg := range req.Arguments {
|
||||
args[arg.Name] = arg.Value
|
||||
}
|
||||
|
|
@ -47,19 +48,103 @@ func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *pb.StartActionRequest)
|
|||
return &pb.StartActionResponse{
|
||||
ExecutionUuid: uuid,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *pb.ExecutionStatusRequest) (*pb.ExecutionStatusResponse, error) {
|
||||
res := &pb.ExecutionStatusResponse{}
|
||||
func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *pb.StartActionAndWaitRequest) (*pb.StartActionAndWaitResponse, error) {
|
||||
args := make(map[string]string)
|
||||
|
||||
logEntry, ok := api.executor.Logs[req.ExecutionUuid]
|
||||
|
||||
if !ok {
|
||||
return res, nil
|
||||
execReq := executor.ExecutionRequest{
|
||||
ActionName: req.ActionName,
|
||||
UUID: uuid.NewString(),
|
||||
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,
|
||||
ActionIcon: logEntry.ActionIcon,
|
||||
DatetimeStarted: logEntry.DatetimeStarted,
|
||||
|
|
@ -74,6 +159,18 @@ func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *pb.ExecutionStatus
|
|||
ExecutionStarted: logEntry.ExecutionStarted,
|
||||
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
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
||||
for uuid, logEntry := range api.executor.Logs {
|
||||
ret.Logs = append(ret.Logs, &pb.LogEntry{
|
||||
ActionTitle: logEntry.ActionTitle,
|
||||
ActionIcon: logEntry.ActionIcon,
|
||||
DatetimeStarted: logEntry.DatetimeStarted,
|
||||
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,
|
||||
})
|
||||
pbLogEntry := internalLogEntryToPb(logEntry)
|
||||
pbLogEntry.ExecutionUuid = uuid
|
||||
|
||||
ret.Logs = append(ret.Logs, pbLogEntry)
|
||||
}
|
||||
|
||||
sorter := func(i, j int) bool {
|
||||
|
|
|
|||
|
|
@ -62,3 +62,15 @@ func buildChoices(choices []config.ActionArgumentChoice) []*pb.ActionArgumentCho
|
|||
|
||||
return ret
|
||||
}
|
||||
|
||||
func findActionByAlias(alias string) *config.Action {
|
||||
for _, action := range cfg.Actions {
|
||||
if action.TitleAlias != "" {
|
||||
if action.TitleAlias == alias {
|
||||
return &action
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue