-
Notifications
You must be signed in to change notification settings - Fork 441
add --hooks-async and --hooks-before-symlink flags #954
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -207,7 +207,8 @@ func main() { | |
flGroupWrite := pflag.Bool("group-write", | ||
envBool(false, "GITSYNC_GROUP_WRITE", "GIT_SYNC_GROUP_WRITE"), | ||
"ensure that all data (repo, worktrees, etc.) is group writable") | ||
flStaleWorktreeTimeout := pflag.Duration("stale-worktree-timeout", envDuration(0, "GITSYNC_STALE_WORKTREE_TIMEOUT"), | ||
flStaleWorktreeTimeout := pflag.Duration("stale-worktree-timeout", | ||
envDuration(0, "GITSYNC_STALE_WORKTREE_TIMEOUT"), | ||
"how long to retain non-current worktrees") | ||
|
||
flExechookCommand := pflag.String("exechook-command", | ||
|
@@ -236,6 +237,13 @@ func main() { | |
envDuration(3*time.Second, "GITSYNC_WEBHOOK_BACKOFF", "GIT_SYNC_WEBHOOK_BACKOFF"), | ||
"the time to wait before retrying a failed webhook") | ||
|
||
flHooksAsync := pflag.Bool("hooks-async", | ||
envBool(true, "GITSYNC_HOOKS_ASYNC", "GIT_SYNC_HOOKS_ASYNC"), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You don't need "GIT_SYNC_HOOKS_ASYNC" - some flags have old names for back-compat, but this is net-new :) |
||
"run hooks asynchronously") | ||
flHooksBeforeSymlink := pflag.Bool("hooks-before-symlink", | ||
envBool(false, "GITSYNC_HOOKS_BEFORE_SYMLINK", "GIT_SYNC_HOOKS_BEFORE_SYMLINK"), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as with the other, you only need the "GITSYNC_" name |
||
"run hooks before creating the symlink (defaults to false)") | ||
|
||
flUsername := pflag.String("username", | ||
envString("", "GITSYNC_USERNAME", "GIT_SYNC_USERNAME"), | ||
"the username to use for git auth") | ||
|
@@ -844,6 +852,7 @@ func main() { | |
hook.NewHookData(), | ||
log, | ||
*flOneTime, | ||
*flHooksAsync, | ||
) | ||
go webhookRunner.Run(context.Background()) | ||
} | ||
|
@@ -868,10 +877,30 @@ func main() { | |
hook.NewHookData(), | ||
log, | ||
*flOneTime, | ||
*flHooksAsync, | ||
) | ||
go exechookRunner.Run(context.Background()) | ||
} | ||
|
||
runHooks := func(hash string) error { | ||
var err error | ||
if exechookRunner != nil { | ||
log.V(3).Info("sending exechook") | ||
err = exechookRunner.Send(hash) | ||
if err != nil { | ||
return err | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we need to Send() on both, even if an error happens. |
||
} | ||
} | ||
if webhookRunner != nil { | ||
log.V(3).Info("sending webhook") | ||
err = webhookRunner.Send(hash) | ||
} | ||
if err != nil { | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
// Setup signal notify channel | ||
sigChan := make(chan os.Signal, 1) | ||
if syncSig != 0 { | ||
|
@@ -912,11 +941,12 @@ func main() { | |
|
||
failCount := 0 | ||
syncCount := uint64(0) | ||
|
||
for { | ||
start := time.Now() | ||
ctx, cancel := context.WithTimeout(context.Background(), *flSyncTimeout) | ||
|
||
if changed, hash, err := git.SyncRepo(ctx, refreshCreds); err != nil { | ||
if changed, hash, err := git.SyncRepo(ctx, refreshCreds, runHooks, *flHooksBeforeSymlink); err != nil { | ||
failCount++ | ||
updateSyncMetrics(metricKeyError, start) | ||
if *flMaxFailures >= 0 && failCount >= *flMaxFailures { | ||
|
@@ -937,11 +967,10 @@ func main() { | |
log.V(3).Info("touched touch-file", "path", absTouchFile) | ||
} | ||
} | ||
if webhookRunner != nil { | ||
webhookRunner.Send(hash) | ||
} | ||
if exechookRunner != nil { | ||
exechookRunner.Send(hash) | ||
// if --hooks-before-symlink is set, these will have already been sent and completed. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since we already pass |
||
// otherwise, we send them now. | ||
if !*flHooksBeforeSymlink { | ||
runHooks(hash) | ||
} | ||
updateSyncMetrics(metricKeySuccess, start) | ||
} else { | ||
|
@@ -961,14 +990,17 @@ func main() { | |
// Assumes that if hook channels are not nil, they will have at | ||
// least one value before getting closed | ||
exitCode := 0 // is 0 if all hooks succeed, else is 1 | ||
if exechookRunner != nil { | ||
if err := exechookRunner.WaitForCompletion(); err != nil { | ||
exitCode = 1 | ||
// This will not be needed if async == false, because the Send func for the hookRunners will wait | ||
if *flHooksAsync { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This smells funny. Why not validate early (with all the other flag validation) that Or maybe it's better to decompose a bit more. Instead of having the HookRunner do the Wait internally, have main() do the wait when it's in synchronous mode. Then the anonymous function you build here can check the flag. That also means that Send() doesn't need to return an error, which simplifies things when you have both web and exec hooks. Does that make sense? I am a bit jetlagged, let me know if I am not being clear :) |
||
if exechookRunner != nil { | ||
if err := exechookRunner.WaitForCompletion(); err != nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking at it more, this is a bit tricky - you might get a result on the channel that indicates that the hook failed, but it will be retried (async). For one-time that's fine, but is it OK in non-one-time mode? If you ask for synchronous hooks, and the hook fails -- do we think that's "good enough" or do we wait for a success? What if success never comes? |
||
exitCode = 1 | ||
} | ||
} | ||
} | ||
if webhookRunner != nil { | ||
if err := webhookRunner.WaitForCompletion(); err != nil { | ||
exitCode = 1 | ||
if webhookRunner != nil { | ||
if err := webhookRunner.WaitForCompletion(); err != nil { | ||
exitCode = 1 | ||
} | ||
} | ||
} | ||
log.DeleteErrorFile() | ||
|
@@ -1716,7 +1748,7 @@ func (git *repoSync) currentWorktree() (worktree, error) { | |
// SyncRepo syncs the repository to the desired ref, publishes it via the link, | ||
// and tries to clean up any detritus. This function returns whether the | ||
// current hash has changed and what the new hash is. | ||
func (git *repoSync) SyncRepo(ctx context.Context, refreshCreds func(context.Context) error) (bool, string, error) { | ||
func (git *repoSync) SyncRepo(ctx context.Context, refreshCreds func(context.Context) error, runHooks func(hash string) error, flHooksBeforeSymlink bool) (bool, string, error) { | ||
git.log.V(3).Info("syncing", "repo", redactURL(git.repo)) | ||
|
||
if err := refreshCreds(ctx); err != nil { | ||
|
@@ -1771,6 +1803,11 @@ func (git *repoSync) SyncRepo(ctx context.Context, refreshCreds func(context.Con | |
// path was different. | ||
changed := (currentHash != remoteHash) || (currentWorktree != git.worktreeFor(currentHash)) | ||
|
||
// Fire hooks if needed. | ||
if flHooksBeforeSymlink { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is running before the new hash is even checked out? |
||
runHooks(remoteHash) | ||
} | ||
|
||
// We have to do at least one fetch, to ensure that parameters like depth | ||
// are set properly. This is cheap when we already have the target hash. | ||
if changed || git.syncCount == 0 { | ||
|
@@ -2537,6 +2574,13 @@ OPTIONS | |
-?, -h, --help | ||
Print help text and exit. | ||
|
||
--hooks-async, $GITSYNC_HOOKS_ASYNC | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also need to update the manual in README.md |
||
Whether to run the --exechook-command asynchronously. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. exec and web |
||
|
||
--hooks-before-symlink, $GITSYNC_HOOKS_BEFORE_SYMLINK | ||
Whether to run the --exechook-command before updating the symlink. Use in combination with --hooks-async set | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. exec and web There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. linewrap to 80 please |
||
to false if you need the hook to finish before the symlink is updated. | ||
|
||
--http-bind <string>, $GITSYNC_HTTP_BIND | ||
The bind address (including port) for git-sync's HTTP endpoint. | ||
The '/' URL of this endpoint is suitable for Kubernetes startup and | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -89,10 +89,10 @@ func (d *hookData) send(newHash string) { | |
} | ||
|
||
// NewHookRunner returns a new HookRunner. | ||
func NewHookRunner(hook Hook, backoff time.Duration, data *hookData, log logintf, oneTime bool) *HookRunner { | ||
hr := &HookRunner{hook: hook, backoff: backoff, data: data, log: log} | ||
if oneTime { | ||
hr.oneTimeResult = make(chan bool, 1) | ||
func NewHookRunner(hook Hook, backoff time.Duration, data *hookData, log logintf, oneTime bool, async bool) *HookRunner { | ||
hr := &HookRunner{hook: hook, backoff: backoff, data: data, log: log, oneTime: oneTime, async: async} | ||
if oneTime || !async { | ||
hr.result = make(chan bool, 1) | ||
} | ||
return hr | ||
} | ||
|
@@ -107,9 +107,13 @@ type HookRunner struct { | |
data *hookData | ||
// Logger | ||
log logintf | ||
// Used to send a status result when running in one-time mode. | ||
// Used to send a status result when running in one-time or non-async mode. | ||
// Should be initialised to a buffered channel of size 1. | ||
oneTimeResult chan bool | ||
result chan bool | ||
// Bool for whether this is a one-time hook or not. | ||
oneTime bool | ||
// Bool for whether this is an async hook or not. | ||
async bool | ||
} | ||
|
||
// Just the logr methods we need in this package. | ||
|
@@ -120,8 +124,17 @@ type logintf interface { | |
} | ||
|
||
// Send sends hash to hookdata. | ||
func (r *HookRunner) Send(hash string) { | ||
func (r *HookRunner) Send(hash string) error { | ||
r.data.send(hash) | ||
if !r.async { | ||
r.log.V(1).Info("waiting for completion", "hash", hash, "name", r.hook.Name()) | ||
err := r.WaitForCompletion() | ||
r.log.V(1).Info("hook completed", "hash", hash, "err", err, "name", r.hook.Name()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We already log below - I don't think we need logging here execpt:
|
||
if err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// Run waits for trigger events from the channel, and run hook when triggered. | ||
|
@@ -144,45 +157,49 @@ func (r *HookRunner) Run(ctx context.Context) { | |
r.log.Error(err, "hook failed", "hash", hash, "retry", r.backoff) | ||
updateHookRunCountMetric(r.hook.Name(), "error") | ||
// don't want to sleep unnecessarily terminating anyways | ||
r.sendOneTimeResultAndTerminate(false) | ||
r.sendResult(false) | ||
time.Sleep(r.backoff) | ||
} else { | ||
updateHookRunCountMetric(r.hook.Name(), "success") | ||
lastHash = hash | ||
r.sendOneTimeResultAndTerminate(true) | ||
r.sendResult(true) | ||
break | ||
} | ||
} | ||
} | ||
} | ||
|
||
// If oneTimeResult is nil, does nothing. Otherwise, forwards the caller | ||
// provided success status (as a boolean) of HookRunner to receivers of | ||
// oneTimeResult, closes said chanel, and terminates this goroutine. | ||
// Using this function to write to oneTimeResult ensures it is only ever | ||
// written to once. | ||
func (r *HookRunner) sendOneTimeResultAndTerminate(completedSuccessfully bool) { | ||
if r.oneTimeResult != nil { | ||
r.oneTimeResult <- completedSuccessfully | ||
close(r.oneTimeResult) | ||
func (r *HookRunner) sendResult(completedSuccessfully bool) { | ||
// if onetime is true, we send the result then exit | ||
if r.oneTime { | ||
r.result <- completedSuccessfully | ||
close(r.result) | ||
runtime.Goexit() | ||
} else if !r.async { | ||
// if onetime is false, and we've set non-async we send but don't exit. | ||
r.result <- completedSuccessfully | ||
} | ||
// if neither oneTime nor !async, we do nothing here. | ||
} | ||
|
||
// WaitForCompletion waits for HookRunner to send completion message to | ||
// calling thread and returns either true if HookRunner executed successfully | ||
// and some error otherwise. | ||
// Assumes that r.oneTimeResult is not nil, but if it is, returns an error. | ||
// Assumes that either r.oneTime or !r.async, otherwise returns an error. | ||
func (r *HookRunner) WaitForCompletion() error { | ||
// Make sure function should be called | ||
if r.oneTimeResult == nil { | ||
if r.result == nil { | ||
return fmt.Errorf("HookRunner.WaitForCompletion called on async runner") | ||
} | ||
|
||
// Perform wait on HookRunner | ||
hookRunnerFinishedSuccessfully := <-r.oneTimeResult | ||
if !hookRunnerFinishedSuccessfully { | ||
return fmt.Errorf("hook completed with error") | ||
// If oneTimeResult is not nil, we wait for its result. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. s/oneTimeResult/result |
||
if r.result != nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you already checked this and returned above |
||
hookRunnerFinishedSuccessfully := <-r.result | ||
r.log.V(1).Info("hook completed", "success", hookRunnerFinishedSuccessfully, | ||
"oneTime", r.oneTime, "async", r.async, "name", r.hook.Name()) | ||
if !hookRunnerFinishedSuccessfully { | ||
return fmt.Errorf("hook completed with error") | ||
} | ||
} | ||
|
||
return nil | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -96,7 +96,7 @@ function assert_file_eq() { | |
if [[ $(cat "$1") == "$2" ]]; then | ||
return | ||
fi | ||
fail "$1 does not contain '$2': $(cat "$1")" | ||
fail "$1 does not equal '$2': $(cat "$1")" | ||
} | ||
|
||
function assert_file_contains() { | ||
|
@@ -2406,7 +2406,7 @@ function e2e::exechook_fail_retry() { | |
--exechook-command="/$EXECHOOK_COMMAND_FAIL" \ | ||
--exechook-backoff=1s \ | ||
& | ||
sleep 3 # give it time to retry | ||
sleep 4 # give it time to retry | ||
|
||
# Check that exechook was called | ||
assert_file_lines_ge "$RUNLOG" 2 | ||
|
@@ -2486,6 +2486,55 @@ function e2e::exechook_startup_after_crash() { | |
assert_file_lines_eq "$RUNLOG" 1 | ||
} | ||
|
||
############################################## | ||
# Test exechook-success with --hooks-async=false | ||
############################################## | ||
function e2e::exechook_success_hooks_non_async() { | ||
cat /dev/null > "$RUNLOG" | ||
|
||
GIT_SYNC \ | ||
--hooks-async=false \ | ||
--period=100ms \ | ||
--repo="file://$REPO" \ | ||
--root="$ROOT" \ | ||
--link="link" \ | ||
--exechook-command="/$EXECHOOK_COMMAND_SLEEPY" \ | ||
& | ||
sleep 5 # give it time to run | ||
assert_link_exists "$ROOT/link" | ||
assert_file_exists "$ROOT/link/file" | ||
assert_file_exists "$ROOT/link/exechook" | ||
assert_file_eq "$ROOT/link/file" "${FUNCNAME[0]}" | ||
assert_file_eq "$ROOT/link/exechook" "${FUNCNAME[0]}" | ||
assert_file_eq "$ROOT/link/exechook-env" "$EXECHOOK_ENVKEY=$EXECHOOK_ENVVAL" | ||
} | ||
|
||
############################################## | ||
# Test exechook-success with --hooks-async=false and --hooks-before-symlink | ||
############################################## | ||
function e2e::exechook_success_hooks_before_symlink_non_async() { | ||
cat /dev/null > "$RUNLOG" | ||
|
||
GIT_SYNC \ | ||
--hooks-async=false \ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. args-under-test go at the end of the list |
||
--hooks-before-symlink \ | ||
--period=100ms \ | ||
--repo="file://$REPO" \ | ||
--root="$ROOT" \ | ||
--link="link" \ | ||
--exechook-command="/$EXECHOOK_COMMAND_SLEEPY" \ | ||
--exechook-backoff=1s \ | ||
& | ||
sleep 5 # give it time to run | ||
assert_link_exists "$ROOT/link" | ||
assert_file_exists "$ROOT/link/file" | ||
assert_file_exists "$ROOT/link/exechook" | ||
assert_file_eq "$ROOT/link/file" "${FUNCNAME[0]}" | ||
assert_file_eq "$ROOT/link/exechook" "${FUNCNAME[0]}" | ||
assert_file_eq "$ROOT/link/exechook-env" "$EXECHOOK_ENVKEY=$EXECHOOK_ENVVAL" | ||
assert_file_absent "$ROOT/link/delaycheck" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am unclear on what this is actually proving? Can you explain? |
||
} | ||
|
||
############################################## | ||
# Test webhook success | ||
############################################## | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why?