diff --git a/cmd/sfncli/error_names.go b/cmd/sfncli/error_names.go index e626a82..c313994 100644 --- a/cmd/sfncli/error_names.go +++ b/cmd/sfncli/error_names.go @@ -16,19 +16,21 @@ import ( // TaskFailureError is the error reported when failing an activity task. type TaskFailureError interface { - error ErrorName() string + ErrorCause() string + + error } // sendTaskFailure handles sending AWS `SendTaskFailure`. func (t TaskRunner) sendTaskFailure(err TaskFailureError) error { - t.logger.ErrorD("send-task-failure", logger.M{"name": err.ErrorName(), "error": err.Error()}) + t.logger.ErrorD("send-task-failure", logger.M{"name": err.ErrorName(), "cause": err.ErrorCause()}) // don't use SendTaskFailureWithContext, since the failure itself could be from the parent // context being cancelled, but we still want to report to AWS the failure of the task. _, sendErr := t.sfnapi.SendTaskFailure(&sfn.SendTaskFailureInput{ - Cause: aws.String(err.Error()), Error: aws.String(err.ErrorName()), + Cause: aws.String(err.ErrorCause()), TaskToken: &t.taskToken, }) if sendErr != nil { @@ -42,74 +44,83 @@ type TaskFailureUnknown struct { error } -func (t TaskFailureUnknown) ErrorName() string { return "sfncli.Unknown" } +func (t TaskFailureUnknown) ErrorName() string { return "sfncli.Unknown" } +func (t TaskFailureUnknown) ErrorCause() string { return t.Error() } // TaskFailureTaskInputNotJSON is used when the input to the task is not a JSON object. type TaskFailureTaskInputNotJSON struct { input string } -func (t TaskFailureTaskInputNotJSON) Error() string { +func (t TaskFailureTaskInputNotJSON) ErrorName() string { return "sfncli.TaskInputNotJSON" } +func (t TaskFailureTaskInputNotJSON) ErrorCause() string { return fmt.Sprintf("task input not valid JSON: '%s'", t.input) } - -func (t TaskFailureTaskInputNotJSON) ErrorName() string { return "sfncli.TaskInputNotJSON" } +func (t TaskFailureTaskInputNotJSON) Error() string { return t.ErrorCause() } // TaskFailureCommandNotFound is used when the command passed to sfncli is not found. type TaskFailureCommandNotFound struct { path string } -func (t TaskFailureCommandNotFound) Error() string { +func (t TaskFailureCommandNotFound) ErrorName() string { return "sfncli.CommandNotFound" } +func (t TaskFailureCommandNotFound) ErrorCause() string { return fmt.Sprintf("command not found: '%s'", t.path) } - -func (t TaskFailureCommandNotFound) ErrorName() string { return "sfncli.CommandNotFound" } +func (t TaskFailureCommandNotFound) Error() string { return t.ErrorCause() } // TaskFailureCommandKilled happens when the command is sent a kill signal by the OS. type TaskFailureCommandKilled struct { stderr string } -func (t TaskFailureCommandKilled) Error() string { return t.stderr } - -func (t TaskFailureCommandKilled) ErrorName() string { return "sfncli.CommandKilled" } +func (t TaskFailureCommandKilled) ErrorName() string { return "sfncli.CommandKilled" } +func (t TaskFailureCommandKilled) ErrorCause() string { return t.stderr } +func (t TaskFailureCommandKilled) Error() string { + return fmt.Sprintf("%s: %s", t.ErrorName(), t.ErrorCause()) +} // TaskFailureCommandKilled happens when the command exits with a nonzero exit code and doesn't specifiy its own error name in the output. type TaskFailureCommandExitedNonzero struct { stderr string } -func (t TaskFailureCommandExitedNonzero) Error() string { return t.stderr } - -func (t TaskFailureCommandExitedNonzero) ErrorName() string { return "sfncli.CommandExitedNonzero" } - -// TaskFailureCustomErrorName happens when the command exits with a nonzero exit code and outputs a custom error name to stdout. -type TaskFailureCustomErrorName struct { - errorName string - stderr string +func (t TaskFailureCommandExitedNonzero) ErrorName() string { return "sfncli.CommandExitedNonzero" } +func (t TaskFailureCommandExitedNonzero) ErrorCause() string { return t.stderr } +func (t TaskFailureCommandExitedNonzero) Error() string { + return fmt.Sprintf("%s: %s", t.ErrorName(), t.ErrorCause()) } -func (t TaskFailureCustomErrorName) Error() string { return t.stderr } +// TaskFailureCustom happens when the command exits with a nonzero exit code and outputs a custom error name to stdout. +type TaskFailureCustom struct { + Err string `json:"error"` + Cause string `json:"cause"` +} -func (t TaskFailureCustomErrorName) ErrorName() string { return t.errorName } +func (t TaskFailureCustom) ErrorName() string { return t.Err } +func (t TaskFailureCustom) ErrorCause() string { return t.Cause } +func (t TaskFailureCustom) Error() string { + return fmt.Sprintf("%s: %s", t.ErrorName(), t.ErrorCause()) +} // TaskFailureTaskOutputNotJSON is used when the output of the task is not a JSON object. type TaskFailureTaskOutputNotJSON struct { output string } -func (t TaskFailureTaskOutputNotJSON) Error() string { +func (t TaskFailureTaskOutputNotJSON) ErrorName() string { return "sfncli.TaskOutputNotJSON" } +func (t TaskFailureTaskOutputNotJSON) ErrorCause() string { return fmt.Sprintf("stdout not valid JSON: '%s'", t.output) } - -func (t TaskFailureTaskOutputNotJSON) ErrorName() string { return "sfncli.TaskOutputNotJSON" } +func (t TaskFailureTaskOutputNotJSON) Error() string { return t.ErrorCause() } // TaskFailureCommandKilled happens when sfncli receives SIGTERM. type TaskFailureCommandTerminated struct { stderr string } -func (t TaskFailureCommandTerminated) Error() string { return t.stderr } - -func (t TaskFailureCommandTerminated) ErrorName() string { return "sfncli.CommandTerminated" } +func (t TaskFailureCommandTerminated) ErrorName() string { return "sfncli.CommandTerminated" } +func (t TaskFailureCommandTerminated) ErrorCause() string { return t.stderr } +func (t TaskFailureCommandTerminated) Error() string { + return fmt.Sprintf("%s: %s", t.ErrorName(), t.ErrorCause()) +} diff --git a/cmd/sfncli/runner.go b/cmd/sfncli/runner.go index dbf304e..0cb9da2 100644 --- a/cmd/sfncli/runner.go +++ b/cmd/sfncli/runner.go @@ -111,11 +111,11 @@ func (t *TaskRunner) Process(ctx context.Context, args []string, input string) e t.logger.InfoD("exec-command-start", logger.M{"args": args, "cmd": t.cmd, "workdirectory": tmpDir}) } if err := t.execCmd.Run(); err != nil { - stderr := strings.TrimSpace(stderrbuf.String()) // remove trailing newline - customErrorName := parseCustomErrorNameFromStdout(stdoutbuf.String()) + stderr := strings.TrimSpace(stderrbuf.String()) // remove trailing newline + customError, _ := parseCustomErrorFromStdout(stdoutbuf.String()) // ignore parsing errors if t.receivedSigterm { - if customErrorName != "" { - return t.sendTaskFailure(TaskFailureCustomErrorName{errorName: customErrorName, stderr: stderr}) + if customError.ErrorName() != "" { + return t.sendTaskFailure(customError) } return t.sendTaskFailure(TaskFailureCommandTerminated{stderr: stderr}) } @@ -123,8 +123,8 @@ func (t *TaskRunner) Process(ctx context.Context, args []string, input string) e case *os.PathError: return t.sendTaskFailure(TaskFailureCommandNotFound{path: err.Path}) case *exec.ExitError: - if customErrorName != "" { - return t.sendTaskFailure(TaskFailureCustomErrorName{errorName: customErrorName, stderr: stderr}) + if customError.ErrorName() != "" { + return t.sendTaskFailure(customError) } status := err.ProcessState.Sys().(syscall.WaitStatus) switch { @@ -201,12 +201,10 @@ func signalProcess(pid int, signal os.Signal) { proc.Signal(signal) } -func parseCustomErrorNameFromStdout(stdout string) string { - var customError struct { - ErrorName string `json:"error_name"` - } - json.Unmarshal([]byte(taskOutputFromStdout(stdout)), &customError) - return customError.ErrorName +func parseCustomErrorFromStdout(stdout string) (TaskFailureCustom, error) { + var customError TaskFailureCustom + err := json.Unmarshal([]byte(taskOutputFromStdout(stdout)), &customError) + return customError, err } func taskOutputFromStdout(stdout string) string { diff --git a/cmd/sfncli/runner_test.go b/cmd/sfncli/runner_test.go index cd20e77..121bb8e 100644 --- a/cmd/sfncli/runner_test.go +++ b/cmd/sfncli/runner_test.go @@ -68,7 +68,7 @@ func TestTaskFailureTaskInputNotJSON(t *testing.T) { defer controller.Finish() mockSFN := mocksfn.NewMockSFNAPI(controller) mockSFN.EXPECT().SendTaskFailure(&sfn.SendTaskFailureInput{ - Cause: aws.String(expectedError.Error()), + Cause: aws.String(expectedError.ErrorCause()), Error: aws.String(expectedError.ErrorName()), TaskToken: aws.String(mockTaskToken), }) @@ -111,7 +111,7 @@ func TestTaskFailureCommandNotFound(t *testing.T) { defer controller.Finish() mockSFN := mocksfn.NewMockSFNAPI(controller) mockSFN.EXPECT().SendTaskFailure(&sfn.SendTaskFailureInput{ - Cause: aws.String(expectedError.Error()), + Cause: aws.String(expectedError.ErrorCause()), Error: aws.String(expectedError.ErrorName()), TaskToken: aws.String(mockTaskToken), }) @@ -132,7 +132,7 @@ func TestTaskFailureCommandKilled(t *testing.T) { defer controller.Finish() mockSFN := mocksfn.NewMockSFNAPI(controller) mockSFN.EXPECT().SendTaskFailure(&sfn.SendTaskFailureInput{ - Cause: aws.String(expectedError.Error()), + Cause: aws.String(expectedError.ErrorCause()), Error: aws.String(expectedError.ErrorName()), TaskToken: aws.String(mockTaskToken), }) @@ -157,7 +157,7 @@ func TestTaskFailureCommandExitedNonzero(t *testing.T) { defer controller.Finish() mockSFN := mocksfn.NewMockSFNAPI(controller) mockSFN.EXPECT().SendTaskFailure(&sfn.SendTaskFailureInput{ - Cause: aws.String(expectedError.Error()), + Cause: aws.String(expectedError.ErrorCause()), Error: aws.String(expectedError.ErrorName()), TaskToken: aws.String(mockTaskToken), }) @@ -171,14 +171,14 @@ func TestTaskFailureCustomErrorName(t *testing.T) { testCtx, testCtxCancel := context.WithCancel(context.Background()) defer testCtxCancel() cmd := "stderr_stdout_exitcode.sh" - cmdArgs := []string{"stderr", `{"error_name": "custom.error_name"}`, "10"} - expectedError := TaskFailureCustomErrorName{errorName: "custom.error_name", stderr: "stderr"} + cmdArgs := []string{"stderr", `{"error": "custom.error_name", "cause": "bar"}`, "10"} + expectedError := TaskFailureCustom{Err: "custom.error_name", Cause: "bar"} controller := gomock.NewController(t) defer controller.Finish() mockSFN := mocksfn.NewMockSFNAPI(controller) mockSFN.EXPECT().SendTaskFailure(&sfn.SendTaskFailureInput{ - Cause: aws.String(expectedError.Error()), + Cause: aws.String(expectedError.ErrorCause()), Error: aws.String(expectedError.ErrorName()), TaskToken: aws.String(mockTaskToken), }) @@ -199,7 +199,7 @@ func TestTaskFailureTaskOutputNotJSON(t *testing.T) { defer controller.Finish() mockSFN := mocksfn.NewMockSFNAPI(controller) mockSFN.EXPECT().SendTaskFailure(&sfn.SendTaskFailureInput{ - Cause: aws.String(expectedError.Error()), + Cause: aws.String(expectedError.ErrorCause()), Error: aws.String(expectedError.ErrorName()), TaskToken: aws.String(mockTaskToken), }) @@ -220,7 +220,7 @@ func TestTaskFailureCommandTerminated(t *testing.T) { defer controller.Finish() mockSFN := mocksfn.NewMockSFNAPI(controller) mockSFN.EXPECT().SendTaskFailure(&sfn.SendTaskFailureInput{ - Cause: aws.String(expectedError.Error()), + Cause: aws.String(expectedError.ErrorCause()), Error: aws.String(expectedError.ErrorName()), TaskToken: aws.String(mockTaskToken), }) @@ -238,14 +238,14 @@ func TestTaskFailureCommandTerminated(t *testing.T) { testCtx, testCtxCancel := context.WithCancel(context.Background()) defer testCtxCancel() cmd := "stderr_stdout_exitcode_onsigterm.sh" - cmdArgs := []string{"stderr", `{"error_name": "custom.error_name"}`, "1"} - expectedError := TaskFailureCustomErrorName{errorName: "custom.error_name", stderr: "stderr"} + cmdArgs := []string{"stderr", `{"error": "custom.error_name", "cause": "foo"}`, "1"} + expectedError := TaskFailureCustom{Err: "custom.error_name", Cause: "foo"} controller := gomock.NewController(t) defer controller.Finish() mockSFN := mocksfn.NewMockSFNAPI(controller) mockSFN.EXPECT().SendTaskFailure(&sfn.SendTaskFailureInput{ - Cause: aws.String(expectedError.Error()), + Cause: aws.String(expectedError.ErrorCause()), Error: aws.String(expectedError.ErrorName()), TaskToken: aws.String(mockTaskToken), }) @@ -270,7 +270,7 @@ func TestTaskFailureCommandTerminated(t *testing.T) { defer controller.Finish() mockSFN := mocksfn.NewMockSFNAPI(controller) mockSFN.EXPECT().SendTaskFailure(&sfn.SendTaskFailureInput{ - Cause: aws.String(expectedError.Error()), + Cause: aws.String(expectedError.ErrorCause()), Error: aws.String(expectedError.ErrorName()), TaskToken: aws.String(mockTaskToken), })