@@ -2,6 +2,7 @@ package hook
2
2
3
3
import (
4
4
"bytes"
5
+ "errors"
5
6
"fmt"
6
7
"io"
7
8
"slices"
@@ -34,15 +35,13 @@ func NewHookExecutor(oplog *oplog.OpLog, bigOutputStore *rotatinglog.RotatingLog
34
35
35
36
// ExecuteHooks schedules tasks for the hooks subscribed to the given event. The vars map is used to substitute variables
36
37
// Hooks are pulled both from the provided plan and from the repo config.
37
- func (e * HookExecutor ) ExecuteHooks (repo * v1.Repo , plan * v1.Plan , snapshotId string , events []v1.Hook_Condition , vars HookVars ) {
38
+ func (e * HookExecutor ) ExecuteHooks (repo * v1.Repo , plan * v1.Plan , events []v1.Hook_Condition , vars HookVars ) error {
38
39
operationBase := v1.Operation {
39
- Status : v1 .OperationStatus_STATUS_INPROGRESS ,
40
- PlanId : plan .GetId (),
41
- RepoId : repo .GetId (),
42
- SnapshotId : snapshotId ,
40
+ Status : v1 .OperationStatus_STATUS_INPROGRESS ,
41
+ PlanId : plan .GetId (),
42
+ RepoId : repo .GetId (),
43
43
}
44
44
45
- vars .SnapshotId = snapshotId
46
45
vars .Repo = repo
47
46
vars .Plan = plan
48
47
vars .CurTime = time .Now ()
@@ -56,14 +55,20 @@ func (e *HookExecutor) ExecuteHooks(repo *v1.Repo, plan *v1.Plan, snapshotId str
56
55
57
56
name := fmt .Sprintf ("repo/%v/hook/%v" , repo .Id , idx )
58
57
operation := proto .Clone (& operationBase ).(* v1.Operation )
58
+ operation .DisplayMessage = "running " + name
59
59
operation .UnixTimeStartMs = curTimeMs ()
60
60
operation .Op = & v1.Operation_OperationRunHook {
61
61
OperationRunHook : & v1.OperationRunHook {
62
62
Name : name ,
63
63
},
64
64
}
65
65
zap .L ().Info ("running hook" , zap .String ("plan" , plan .Id ), zap .Int64 ("opId" , operation .Id ), zap .String ("hook" , name ))
66
- e .executeHook (operation , h , event , vars )
66
+ if err := e .executeHook (operation , h , event , vars ); err != nil {
67
+ zap .S ().Errorf ("error on repo hook %v on condition %v: %v" , idx , event .String (), err )
68
+ if isHaltingError (err ) {
69
+ return fmt .Errorf ("repo hook %v on condition %v: %w" , idx , event .String (), err )
70
+ }
71
+ }
67
72
}
68
73
69
74
for idx , hook := range plan .GetHooks () {
@@ -75,15 +80,23 @@ func (e *HookExecutor) ExecuteHooks(repo *v1.Repo, plan *v1.Plan, snapshotId str
75
80
76
81
name := fmt .Sprintf ("plan/%v/hook/%v" , plan .Id , idx )
77
82
operation := proto .Clone (& operationBase ).(* v1.Operation )
83
+ operation .DisplayMessage = "running " + name
78
84
operation .UnixTimeStartMs = curTimeMs ()
79
85
operation .Op = & v1.Operation_OperationRunHook {
80
86
OperationRunHook : & v1.OperationRunHook {
81
87
Name : name ,
82
88
},
83
89
}
90
+
84
91
zap .L ().Info ("running hook" , zap .String ("plan" , plan .Id ), zap .Int64 ("opId" , operation .Id ), zap .String ("hook" , name ))
85
- e .executeHook (operation , h , event , vars )
92
+ if err := e .executeHook (operation , h , event , vars ); err != nil {
93
+ zap .S ().Errorf ("error on plan hook %v on condition %v: %v" , idx , event .String (), err )
94
+ if isHaltingError (err ) {
95
+ return fmt .Errorf ("plan hook %v on condition %v: %w" , idx , event .String (), err )
96
+ }
97
+ }
86
98
}
99
+ return nil
87
100
}
88
101
89
102
func firstMatchingCondition (hook * Hook , events []v1.Hook_Condition ) v1.Hook_Condition {
@@ -95,35 +108,43 @@ func firstMatchingCondition(hook *Hook, events []v1.Hook_Condition) v1.Hook_Cond
95
108
return v1 .Hook_CONDITION_UNKNOWN
96
109
}
97
110
98
- func (e * HookExecutor ) executeHook (op * v1.Operation , hook * Hook , event v1.Hook_Condition , vars HookVars ) {
111
+ func (e * HookExecutor ) executeHook (op * v1.Operation , hook * Hook , event v1.Hook_Condition , vars HookVars ) error {
99
112
if err := e .oplog .Add (op ); err != nil {
100
113
zap .S ().Errorf ("execute hook: add operation: %v" , err )
101
- return
114
+ return errors . New ( "couldn't create operation" )
102
115
}
103
116
104
117
output := & bytes.Buffer {}
118
+ fmt .Fprintf (output , "triggering condition: %v\n " , event .String ())
105
119
120
+ var retErr error
106
121
if err := hook .Do (event , vars , io .MultiWriter (output )); err != nil {
107
122
output .Write ([]byte (fmt .Sprintf ("Error: %v" , err )))
108
- op .DisplayMessage = err .Error ()
109
- op .Status = v1 .OperationStatus_STATUS_ERROR
110
- zap .S ().Errorf ("execute hook: %v" , err )
123
+ err = applyHookErrorPolicy (hook .OnError , err )
124
+ var cancelErr * HookErrorRequestCancel
125
+ if errors .As (err , & cancelErr ) {
126
+ // if it was a cancel then it successfully indicated it's intent to the caller
127
+ // no error should be displayed in the UI.
128
+ op .Status = v1 .OperationStatus_STATUS_SUCCESS
129
+ } else {
130
+ op .Status = v1 .OperationStatus_STATUS_ERROR
131
+ }
132
+ retErr = err
111
133
} else {
112
134
op .Status = v1 .OperationStatus_STATUS_SUCCESS
113
135
}
114
136
115
137
outputRef , err := e .logStore .Write (output .Bytes ())
116
138
if err != nil {
117
- zap .S ().Errorf ("execute hook: write log: %v" , err )
118
- return
139
+ retErr = errors .Join (retErr , fmt .Errorf ("write logstore: %w" , err ))
119
140
}
120
- op .Op .( * v1. Operation_OperationRunHook ). OperationRunHook . OutputLogref = outputRef
141
+ op .Logref = outputRef
121
142
122
143
op .UnixTimeEndMs = curTimeMs ()
123
144
if err := e .oplog .Update (op ); err != nil {
124
- zap .S ().Errorf ("execute hook: update operation: %v" , err )
125
- return
145
+ retErr = errors .Join (retErr , fmt .Errorf ("update oplog: %w" , err ))
126
146
}
147
+ return retErr
127
148
}
128
149
129
150
func curTimeMs () int64 {
@@ -175,3 +196,23 @@ func (h *Hook) renderTemplateOrDefault(template string, defaultTmpl string, vars
175
196
}
176
197
return h .renderTemplate (template , vars )
177
198
}
199
+
200
+ func applyHookErrorPolicy (onError v1.Hook_OnError , err error ) error {
201
+ if err == nil || errors .As (err , & HookErrorFatal {}) || errors .As (err , & HookErrorRequestCancel {}) {
202
+ return err
203
+ }
204
+
205
+ if onError == v1 .Hook_ON_ERROR_CANCEL {
206
+ return & HookErrorRequestCancel {Err : err }
207
+ } else if onError == v1 .Hook_ON_ERROR_FATAL {
208
+ return & HookErrorFatal {Err : err }
209
+ }
210
+ return err
211
+ }
212
+
213
+ // isHaltingError returns true if the error is a fatal error or a request to cancel the operation
214
+ func isHaltingError (err error ) bool {
215
+ var fatalErr * HookErrorFatal
216
+ var cancelErr * HookErrorRequestCancel
217
+ return errors .As (err , & fatalErr ) || errors .As (err , & cancelErr )
218
+ }
0 commit comments