From 90e1f3d40b979f3399084857d1a99a22e95c199e Mon Sep 17 00:00:00 2001 From: lucamrgs <39555424+lucamrgs@users.noreply.github.com> Date: Tue, 23 Jul 2024 14:43:11 +0200 Subject: [PATCH] Feature/181 asynchronous return execution (#183) --- internal/decomposer/decomposer.go | 30 ++++++++++++- routes/trigger/trigger_api.go | 43 +++++++++++++------ .../playbook_action_executor_test.go | 2 + .../mocks/mock_decomposer/mock_decomposer.go | 8 ++++ .../routes/trigger_api/tigger_api_test.go | 12 ++++-- 5 files changed, 78 insertions(+), 17 deletions(-) diff --git a/internal/decomposer/decomposer.go b/internal/decomposer/decomposer.go index d06a025b..0eb201d7 100644 --- a/internal/decomposer/decomposer.go +++ b/internal/decomposer/decomposer.go @@ -34,6 +34,7 @@ type ExecutionDetails struct { } type IDecomposer interface { + ExecuteAsync(playbook cacao.Playbook, detailsch chan ExecutionDetails) Execute(playbook cacao.Playbook) (*ExecutionDetails, error) } @@ -69,11 +70,35 @@ type Decomposer struct { } // Execute a Playbook -func (decomposer *Decomposer) Execute(playbook cacao.Playbook) (*ExecutionDetails, error) { +func (decomposer *Decomposer) ExecuteAsync(playbook cacao.Playbook, detailsch chan ExecutionDetails) { executionId := decomposer.guid.New() log.Debugf("Starting execution %s for Playbook %s", executionId, playbook.ID) + details := ExecutionDetails{executionId, playbook.ID, playbook.PlaybookVariables} + decomposer.details = details + + if detailsch != nil { + detailsch <- details + } + + _ = decomposer.execute(playbook) + +} + +func (decomposer *Decomposer) Execute(playbook cacao.Playbook) (*ExecutionDetails, error) { + + executionId := decomposer.guid.New() + log.Debugf("Starting execution %s for Playbook %s", executionId, playbook.ID) decomposer.details = ExecutionDetails{executionId, playbook.ID, playbook.PlaybookVariables} + + err := decomposer.execute(playbook) + + return &decomposer.details, err + +} + +func (decomposer *Decomposer) execute(playbook cacao.Playbook) error { + decomposer.playbook = playbook stepId := playbook.WorkflowStart @@ -88,7 +113,8 @@ func (decomposer *Decomposer) Execute(playbook cacao.Playbook) (*ExecutionDetail decomposer.details.Variables = outputVariables // Reporting workflow end decomposer.reporter.ReportWorkflowEnd(decomposer.details.ExecutionId, playbook, err) - return &decomposer.details, err + + return err } // Execute a Workflow branch of a Playbook diff --git a/routes/trigger/trigger_api.go b/routes/trigger/trigger_api.go index b8445e6b..2bb392cd 100644 --- a/routes/trigger/trigger_api.go +++ b/routes/trigger/trigger_api.go @@ -4,8 +4,10 @@ import ( "io" "net/http" "reflect" + "time" "soarca/internal/controller/decomposer_controller" + "soarca/internal/decomposer" "soarca/logger" "soarca/models/decoder" "soarca/routes/error" @@ -26,12 +28,15 @@ func init() { } type TriggerApi struct { - controller decomposer_controller.IController + controller decomposer_controller.IController + Executionsch chan decomposer.ExecutionDetails } func New(controller decomposer_controller.IController) *TriggerApi { instance := TriggerApi{} instance.controller = controller + // Channel to get back execution details + instance.Executionsch = make(chan decomposer.ExecutionDetails) return &instance } @@ -54,17 +59,31 @@ func (trigger *TriggerApi) Execute(context *gin.Context) { "POST /trigger/playbook", "") return } - executionDetail, errDecomposer := decomposer.Execute(*playbook) - if errDecomposer != nil { - error.SendErrorResponse(context, http.StatusBadRequest, - "Failed to decode playbook", - "POST /trigger/playbook", - executionDetail.ExecutionId.String()) - } else { - msg := gin.H{ - "execution_id": executionDetail.ExecutionId.String(), - "payload": executionDetail.PlaybookId, + + go decomposer.ExecuteAsync(*playbook, trigger.Executionsch) + + // Hard coding the timer to return execution id + timer := time.NewTimer(time.Duration(3) * time.Second) + for { + select { + case <-timer.C: + msg := gin.H{ + "execution_id": nil, + "payload": playbook.ID, + } + context.JSON(http.StatusRequestTimeout, msg) + log.Error("async execution timed out for playbook ", playbook.ID) + case exec_details := <-trigger.Executionsch: + playbook_id := exec_details.PlaybookId + exec_id := exec_details.ExecutionId + if playbook_id == playbook.ID { + msg := gin.H{ + "execution_id": exec_id, + "payload": playbook_id, + } + context.JSON(http.StatusOK, msg) + return + } } - context.JSON(http.StatusOK, msg) } } diff --git a/test/unittest/executor/playbook_action/playbook_action_executor_test.go b/test/unittest/executor/playbook_action/playbook_action_executor_test.go index ef97fe43..41c13b78 100644 --- a/test/unittest/executor/playbook_action/playbook_action_executor_test.go +++ b/test/unittest/executor/playbook_action/playbook_action_executor_test.go @@ -79,10 +79,12 @@ func TestExecutePlaybook(t *testing.T) { Variables: cacao.NewVariables(returnedVariables)} playbook2 := cacao.Playbook{ID: playbookId, PlaybookVariables: cacao.NewVariables(expectedVariables)} + mockDecomposer.On("Execute", playbook2).Return(&details, nil) results, err := executerObject.Execute(metadata, step, cacao.NewVariables(addedVariables)) + mockDecomposer.AssertExpectations(t) mock_reporter.AssertExpectations(t) assert.Equal(t, err, nil) assert.Equal(t, results, cacao.NewVariables(returnedVariables)) diff --git a/test/unittest/mocks/mock_decomposer/mock_decomposer.go b/test/unittest/mocks/mock_decomposer/mock_decomposer.go index 9d1a4d8d..f3f6d5d9 100644 --- a/test/unittest/mocks/mock_decomposer/mock_decomposer.go +++ b/test/unittest/mocks/mock_decomposer/mock_decomposer.go @@ -4,6 +4,7 @@ import ( "soarca/internal/decomposer" "soarca/models/cacao" + "github.com/google/uuid" "github.com/stretchr/testify/mock" ) @@ -11,6 +12,13 @@ type Mock_Decomposer struct { mock.Mock } +func (mock *Mock_Decomposer) ExecuteAsync(playbook cacao.Playbook, detailsch chan decomposer.ExecutionDetails) { + args := mock.Called(playbook, detailsch) + if detailsch != nil { + details := decomposer.ExecutionDetails{ExecutionId: args.Get(2).(uuid.UUID), PlaybookId: playbook.ID, Variables: cacao.NewVariables()} + detailsch <- details + } +} func (mock *Mock_Decomposer) Execute(playbook cacao.Playbook) (*decomposer.ExecutionDetails, error) { args := mock.Called(playbook) return args.Get(0).(*decomposer.ExecutionDetails), args.Error(1) diff --git a/test/unittest/routes/trigger_api/tigger_api_test.go b/test/unittest/routes/trigger_api/tigger_api_test.go index cedefe5b..f79e0d58 100644 --- a/test/unittest/routes/trigger_api/tigger_api_test.go +++ b/test/unittest/routes/trigger_api/tigger_api_test.go @@ -17,9 +17,10 @@ import ( "github.com/gin-gonic/gin" "github.com/go-playground/assert/v2" + "github.com/google/uuid" ) -func TestExecutionOfPlaybook(t *testing.T) { +func TestTriggerExecutionOfPlaybook(t *testing.T) { jsonFile, err := os.Open("../playbook.json") if err != nil { fmt.Println(err) @@ -34,17 +35,22 @@ func TestExecutionOfPlaybook(t *testing.T) { mock_controller := new(mock_decomposer_controller.Mock_Controller) mock_controller.On("NewDecomposer").Return(mock_decomposer) playbook := cacao.Decode(byteValue) - mock_decomposer.On("Execute", *playbook).Return(&decomposer.ExecutionDetails{}, nil) - recorder := httptest.NewRecorder() trigger_api := trigger.New(mock_controller) + recorder := httptest.NewRecorder() trigger.Routes(app, trigger_api) + executionId, _ := uuid.Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8") + mock_decomposer.On("ExecuteAsync", *playbook, trigger_api.Executionsch).Return(&decomposer.ExecutionDetails{}, nil, executionId) + request, err := http.NewRequest("POST", "/trigger/playbook", bytes.NewBuffer(byteValue)) if err != nil { t.Fail() } + + expected_return_string := `{"execution_id":"6ba7b810-9dad-11d1-80b4-00c04fd430c8","payload":"playbook--61a6c41e-6efc-4516-a242-dfbc5c89d562"}` app.ServeHTTP(recorder, request) + assert.Equal(t, expected_return_string, recorder.Body.String()) assert.Equal(t, 200, recorder.Code) mock_decomposer.AssertExpectations(t) }