diff --git a/OliveTin.proto b/OliveTin.proto index f320ebe..bfdc8f6 100644 --- a/OliveTin.proto +++ b/OliveTin.proto @@ -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" diff --git a/config.yaml b/config.yaml index 1bc6761..e4f319e 100644 --- a/config.yaml +++ b/config.yaml @@ -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' diff --git a/internal/config/config.go b/internal/config/config.go index 6ea2536..e9f7d5e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -5,6 +5,7 @@ package config type Action struct { ID string Title string + TitleAlias string Icon string Shell string ShellAfterCompleted string diff --git a/internal/executor/executor.go b/internal/executor/executor.go index 2fed3d0..779ae61 100644 --- a/internal/executor/executor.go +++ b/internal/executor/executor.go @@ -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 diff --git a/internal/grpcapi/grpcApi.go b/internal/grpcapi/grpcApi.go index 8b253d7..617b135 100644 --- a/internal/grpcapi/grpcApi.go +++ b/internal/grpcapi/grpcApi.go @@ -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 { diff --git a/internal/grpcapi/grpcApiActions.go b/internal/grpcapi/grpcApiActions.go index 5fb1f4b..6b073e1 100644 --- a/internal/grpcapi/grpcApiActions.go +++ b/internal/grpcapi/grpcApiActions.go @@ -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 +}