Pārlūkot izejas kodu

feature: Additional APIs for StartAction (#188)

James Read 2 gadi atpakaļ
vecāks
revīzija
d0f74c1ab7

+ 45 - 0
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"

+ 1 - 0
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'
 

+ 1 - 0
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

+ 17 - 13
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

+ 110 - 24
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) StartActionAndWait(ctx ctx.Context, req *pb.StartActionAndWaitRequest) (*pb.StartActionAndWaitResponse, error) {
+	args := make(map[string]string)
 
+	execReq := executor.ExecutionRequest{
+		ActionName:        req.ActionName,
+		UUID:              uuid.NewString(),
+		Arguments:         args,
+		AuthenticatedUser: acl.UserFromContext(ctx, cfg),
+		Cfg:               cfg,
+	}
+
+	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) ExecutionStatus(ctx ctx.Context, req *pb.ExecutionStatusRequest) (*pb.ExecutionStatusResponse, error) {
-	res := &pb.ExecutionStatusResponse{}
+func (api *oliveTinAPI) StartActionByAlias(ctx ctx.Context, req *pb.StartActionByAliasRequest) (*pb.StartActionByAliasResponse, error) {
+	args := make(map[string]string)
 
-	logEntry, ok := api.executor.Logs[req.ExecutionUuid]
+	action := findActionByAlias(req.ActionAlias)
 
-	if !ok {
-		return res, nil
+	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,
 	}
 
-	res.LogEntry = &pb.LogEntry{
+	_, 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 {

+ 12 - 0
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
+}