Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,22 @@ OPTIONS
will take precedence. If not specified, this defaults to 10
seconds ("10s").

--pre-exechook-backoff <duration>, $GITSYNC_PRE_EXECHOOK_BACKOFF
The time to wait before retrying a failed --pre-exechook-command. If
not specified, this defaults to 3 seconds ("3s").

--pre-exechook-command <string>, $GITSYNC_PRE_EXECHOOK_COMMAND
An optional command to be executed after syncing a new hash of the
remote repository but before publishing the symlink (see --link).
This command does not take any arguments and
executes with the synced repo as its working directory. The
$GITSYNC_HASH environment variable will be set to the previous git hash that
was synced. This hook will always be invoked as it runs before any sync attempt.

--pre-exechook-timeout <duration>, $GITSYNC_PRE_EXECHOOK_TIMEOUT
The timeout for the --pre-exechook-command. If not specifid, this
defaults to 30 seconds ("30s").

--ref <string>, $GITSYNC_REF
The git revision (branch, tag, or hash) to check out. If not
specified, this defaults to "HEAD" (of the upstream repo's default
Expand Down
117 changes: 105 additions & 12 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ type repoSync struct {
appTokenExpiry time.Time // time when github app auth token expires
}

// syncHooks manages the refresh of credentials and hooks.
type syncHooks struct {
refreshCreds func(ctx context.Context) error
beforePublish func(hash string) error
afterPublish func(hash string) error
}

func main() {
// In case we come up as pid 1, act as init.
if os.Getpid() == 1 {
Expand Down Expand Up @@ -230,6 +237,16 @@ func main() {
envDuration(3*time.Second, "GITSYNC_EXECHOOK_BACKOFF", "GIT_SYNC_EXECHOOK_BACKOFF"),
"the time to wait before retrying a failed exechook")

flPreExechookCommand := pflag.String("pre-exechook-command",
envString("", "GITSYNC_PRE_EXECHOOK_COMMAND", "GIT_SYNC_PRE_EXECHOOK_COMMAND"),
"an optional command to be run before syncs complete (must be idempotent)")
flPreExechookTimeout := pflag.Duration("pre-exechook-timeout",
envDuration(30*time.Second, "GITSYNC_PRE_EXECHOOK_TIMEOUT", "GIT_SYNC_PRE_EXECHOOK_TIMEOUT"),
"the timeout for the pre-exechook")
flPreExechookBackoff := pflag.Duration("pre-exechook-backoff",
envDuration(3*time.Second, "GITSYNC_PRE_EXECHOOK_BACKOFF", "GIT_SYNC_PRE_EXECHOOK_BACKOFF"),
"the time to wait before retrying a failed pre-exechook")

flWebhookURL := pflag.String("webhook-url",
envString("", "GITSYNC_WEBHOOK_URL", "GIT_SYNC_WEBHOOK_URL"),
"a URL for optional webhook notifications when syncs complete (must be idempotent)")
Expand Down Expand Up @@ -541,6 +558,15 @@ func main() {
}
}

if *flPreExechookCommand != "" {
if *flPreExechookTimeout < time.Second {
fatalConfigErrorf(log, true, "invalid flag: --pre-exechook-timeout must be at least 1s")
}
if *flPreExechookBackoff < time.Second {
fatalConfigErrorf(log, true, "invalid flag: --pre-exechook-backoff must be at least 1s")
}
}

if *flWebhookURL != "" {
if *flWebhookStatusSuccess == -1 {
// Back-compat: -1 and 0 mean the same things
Expand Down Expand Up @@ -859,8 +885,10 @@ func main() {
// Startup exechooks goroutine
var exechookRunner *hook.HookRunner
if *flExechookCommand != "" {
log := log.WithName("exechook")
logname := "exechook"
log := log.WithName(logname)
exechook := hook.NewExechook(
logname,
cmd.NewRunner(log),
*flExechookCommand,
func(hash string) string {
Expand All @@ -880,6 +908,32 @@ func main() {
go exechookRunner.Run(context.Background())
}

// Startup pre-exechooks goroutine
var preExechookRunner *hook.HookRunner
if *flPreExechookCommand != "" {
logname := "pre-exechook"
log := log.WithName(logname)
exechook := hook.NewExechook(
logname,
cmd.NewRunner(log),
*flPreExechookCommand,
func(hash string) string {
return git.worktreeFor(hash).Path().String()
},
[]string{},
*flPreExechookTimeout,
log,
)
preExechookRunner = hook.NewHookRunner(
exechook,
*flPreExechookBackoff,
hook.NewHookData(),
log,
*flOneTime,
)
go preExechookRunner.Run(context.Background())
}

// Setup signal notify channel
sigChan := make(chan os.Signal, 1)
if syncSig != 0 {
Expand Down Expand Up @@ -931,6 +985,24 @@ func main() {
return nil
}

syncHooks := syncHooks{
refreshCreds: refreshCreds,
beforePublish: func(hash string) error {
if preExechookRunner != nil {
preExechookRunner.Send(hash)
}
return nil
},
afterPublish: func(hash string) error {
if exechookRunner != nil {
exechookRunner.Send(hash)
}
if webhookRunner != nil {
webhookRunner.Send(hash)
}
return nil
},
}
failCount := 0
syncCount := uint64(0)
initialSyncDone := false
Expand All @@ -948,7 +1020,7 @@ func main() {
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, syncHooks); err != nil {
failCount++
updateSyncMetrics(metricKeyError, start)
if maxFails := getMaxFailures(); maxFails >= 0 && failCount >= maxFails {
Expand All @@ -975,12 +1047,6 @@ func main() {
log.V(3).Info("touched touch-file", "path", absTouchFile)
}
}
if webhookRunner != nil {
webhookRunner.Send(hash)
}
if exechookRunner != nil {
exechookRunner.Send(hash)
}
updateSyncMetrics(metricKeySuccess, start)
} else {
updateSyncMetrics(metricKeyNoOp, start)
Expand Down Expand Up @@ -1754,10 +1820,10 @@ 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, syncHooks syncHooks) (bool, string, error) {
git.log.V(3).Info("syncing", "repo", redactURL(git.repo))

if err := refreshCreds(ctx); err != nil {
if err := syncHooks.refreshCreds(ctx); err != nil {
return false, "", fmt.Errorf("credential refresh failed: %w", err)
}

Expand Down Expand Up @@ -1842,7 +1908,12 @@ func (git *repoSync) SyncRepo(ctx context.Context, refreshCreds func(context.Con

// If we have a new hash, update the symlink to point to the new worktree.
if changed {
err := git.publishSymlink(newWorktree)
err := syncHooks.beforePublish(newWorktree.Hash())
if err != nil {
return false, "", err
}

err = git.publishSymlink(newWorktree)
if err != nil {
return false, "", err
}
Expand All @@ -1855,6 +1926,11 @@ func (git *repoSync) SyncRepo(ctx context.Context, refreshCreds func(context.Con
}
}

err := syncHooks.afterPublish(newWorktree.Hash())
if err != nil {
return false, "", err
}

// Mark ourselves as "ready".
setRepoReady()
git.syncCount++
Expand Down Expand Up @@ -2515,7 +2591,8 @@ OPTIONS

--exechook-command <string>, $GITSYNC_EXECHOOK_COMMAND
An optional command to be executed after syncing a new hash of the
remote repository. This command does not take any arguments and
remote repository and publishing the symlink (see --link).
This command does not take any arguments and
executes with the synced repo as its working directory. The
$GITSYNC_HASH environment variable will be set to the git hash that
was synced. If, at startup, git-sync finds that the --root already
Expand Down Expand Up @@ -2676,6 +2753,22 @@ OPTIONS
will take precedence. If not specified, this defaults to 10
seconds ("10s").

--pre-exechook-backoff <duration>, $GITSYNC_PRE_EXECHOOK_BACKOFF
The time to wait before retrying a failed --pre-exechook-command. If
not specified, this defaults to 3 seconds ("3s").

--pre-exechook-command <string>, $GITSYNC_PRE_EXECHOOK_COMMAND
An optional command to be executed after syncing a new hash of the
remote repository but before publishing the symlink (see --link).
This command does not take any arguments and
executes with the synced repo as its working directory. The
$GITSYNC_HASH environment variable will be set to the previous git hash that
was synced. This hook will always be invoked as it runs before any sync attempt.

--pre-exechook-timeout <duration>, $GITSYNC_PRE_EXECHOOK_TIMEOUT
The timeout for the --pre-exechook-command. If not specifid, this
defaults to 30 seconds ("30s").

--ref <string>, $GITSYNC_REF
The git revision (branch, tag, or hash) to check out. If not
specified, this defaults to "HEAD" (of the upstream repo's default
Expand Down
11 changes: 7 additions & 4 deletions pkg/hook/exechook.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import (

// Exechook implements Hook in terms of executing a command.
type Exechook struct {
// Name
name string
// Runner
cmdrunner cmd.Runner
// Command to run
Expand All @@ -42,8 +44,9 @@ type Exechook struct {
}

// NewExechook returns a new Exechook.
func NewExechook(cmdrunner cmd.Runner, command string, getWorktree func(string) string, args []string, timeout time.Duration, log logintf) *Exechook {
func NewExechook(name string, cmdrunner cmd.Runner, command string, getWorktree func(string) string, args []string, timeout time.Duration, log logintf) *Exechook {
return &Exechook{
name: name,
cmdrunner: cmdrunner,
command: command,
getWorktree: getWorktree,
Expand All @@ -55,7 +58,7 @@ func NewExechook(cmdrunner cmd.Runner, command string, getWorktree func(string)

// Name describes hook, implements Hook.Name.
func (h *Exechook) Name() string {
return "exechook"
return h.name
}

// Do runs exechook.command, implements Hook.Do.
Expand All @@ -68,10 +71,10 @@ func (h *Exechook) Do(ctx context.Context, hash string) error {
env := os.Environ()
env = append(env, envKV("GITSYNC_HASH", hash))

h.log.V(0).Info("running exechook", "hash", hash, "command", h.command, "timeout", h.timeout)
h.log.V(0).Info("running hook", "name", h.name, "hash", hash, "command", h.command, "timeout", h.timeout)
stdout, stderr, err := h.cmdrunner.Run(ctx, worktreePath, env, h.command, h.args...)
if err == nil {
h.log.V(1).Info("exechook succeeded", "hash", hash, "stdout", stdout, "stderr", stderr)
h.log.V(1).Info("hook succeeded", "name", h.name, "hash", hash, "stdout", stdout, "stderr", stderr)
}
return err
}
Expand Down
3 changes: 3 additions & 0 deletions pkg/hook/exechook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func TestNotZeroReturnExechookDo(t *testing.T) {
t.Run("test not zero return code", func(t *testing.T) {
l := logging.New("", "", 0)
ch := NewExechook(
"exechook",
cmd.NewRunner(l),
"false",
func(string) string { return "/tmp" },
Expand All @@ -47,6 +48,7 @@ func TestZeroReturnExechookDo(t *testing.T) {
t.Run("test zero return code", func(t *testing.T) {
l := logging.New("", "", 0)
ch := NewExechook(
"exechook",
cmd.NewRunner(l),
"true",
func(string) string { return "/tmp" },
Expand All @@ -65,6 +67,7 @@ func TestTimeoutExechookDo(t *testing.T) {
t.Run("test timeout", func(t *testing.T) {
l := logging.New("", "", 0)
ch := NewExechook(
"exechook",
cmd.NewRunner(l),
"/bin/sh",
func(string) string { return "/tmp" },
Expand Down