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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,23 @@ OPTIONS
will take precedence. If not specified, this defaults to 10
seconds ("10s").

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

--pre-publish-exechook-command <string>, $GITSYNC_PRE_PUBLISH_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-publish-exechook-timeout <duration>, $GITSYNC_PRE_PUBLISH_EXECHOOK_TIMEOUT
The timeout for the --pre-publish-exechook-command. If not
specified 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
143 changes: 124 additions & 19 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")

flPrePubExechookCommand := pflag.String("pre-publish-exechook-command",
envString("", "GITSYNC_PRE_PUBLISH_EXECHOOK_COMMAND"),
"an optional command to be run before syncs complete (must be idempotent)")
flPrePubExechookTimeout := pflag.Duration("pre-publish-exechook-timeout",
envDuration(30*time.Second, "GITSYNC_PRE_PUBLISH_EXECHOOK_TIMEOUT"),
"the timeout for the pre-publish-exechook")
flPrePubExechookBackoff := pflag.Duration("pre-publish-exechook-backoff",
envDuration(3*time.Second, "GITSYNC_PRE_PUBLISH_EXECHOOK_BACKOFF"),
"the time to wait before retrying a failed pre-publish-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 *flPrePubExechookCommand != "" {
if *flPrePubExechookTimeout < time.Second {
fatalConfigErrorf(log, true, "invalid flag: --pre-publish-exechook-timeout must be at least 1s")
}
if *flPrePubExechookBackoff < time.Second {
fatalConfigErrorf(log, true, "invalid flag: --pre-publish-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-publish-exechooks goroutine
var prePubExechookRunner *hook.HookRunner
if *flPrePubExechookCommand != "" {
logname := "pre-publish-exechook"
log := log.WithName(logname)
exechook := hook.NewExechook(
logname,
cmd.NewRunner(log),
*flPrePubExechookCommand,
func(hash string) string {
return git.worktreeFor(hash).Path().String()
},
[]string{},
*flPrePubExechookTimeout,
log,
)
prePubExechookRunner = hook.NewHookRunner(
exechook,
*flPrePubExechookBackoff,
hook.NewHookData(),
log,
*flOneTime,
)
go prePubExechookRunner.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 prePubExechookRunner != nil {
prePubExechookRunner.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 All @@ -999,6 +1065,11 @@ 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 prePubExechookRunner != nil && changed {
if err := prePubExechookRunner.WaitForCompletion(); err != nil {
exitCode = 1
}
}
if exechookRunner != nil {
if err := exechookRunner.WaitForCompletion(); err != nil {
exitCode = 1
Expand Down Expand Up @@ -1754,10 +1825,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 +1913,18 @@ 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 {
Comment thread
thockin marked this conversation as resolved.
err := git.publishSymlink(newWorktree)
// If the previous run crashed before publishing the link, then we
// must call the pre-publish hook, and since changed is true, we will.
// we will. If the previous run crashed after publishing the link,
// then we do not need to call the pre-publish hook, and since
// changed is false, we won't. The post-publish hooks are called in
// both cases.
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 +1937,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,14 +2602,15 @@ 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
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
has the correct hash, this hook will still be invoked. This means
that hooks can be invoked more than one time per hash, so they
must be idempotent. This flag obsoletes --sync-hook-command, but
if sync-hook-command is specified, it will take precedence.
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 has the correct
hash, this hook will still be invoked. This means that hooks can
be invoked more than one time per hash, so they must be idempotent.
This flag obsoletes --sync-hook-command, but if sync-hook-command
is specified, it will take precedence.

--exechook-timeout <duration>, $GITSYNC_EXECHOOK_TIMEOUT
The timeout for the --exechook-command. If not specifid, this
Expand Down Expand Up @@ -2676,6 +2764,23 @@ OPTIONS
will take precedence. If not specified, this defaults to 10
seconds ("10s").

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

--pre-publish-exechook-command <string>, $GITSYNC_PRE_PUBLISH_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-publish-exechook-timeout <duration>, $GITSYNC_PRE_PUBLISH_EXECHOOK_TIMEOUT
The timeout for the --pre-publish-exechook-command. If not
specified 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
7 changes: 5 additions & 2 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 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
Loading
Loading