diff --git a/cmd/git-sync/main.go b/cmd/git-sync/main.go index 87bd21f..c32b21c 100644 --- a/cmd/git-sync/main.go +++ b/cmd/git-sync/main.go @@ -20,6 +20,7 @@ package main // import "k8s.io/git-sync/cmd/git-sync" import ( "bytes" + "context" "flag" "fmt" "io" @@ -52,6 +53,8 @@ var flDest = flag.String("dest", envString("GIT_SYNC_DEST", ""), "the name at which to publish the checked-out files under --root (defaults to leaf dir of --repo)") var flWait = flag.Float64("wait", envFloat("GIT_SYNC_WAIT", 0), "the number of seconds between syncs") +var flSyncTimeout = flag.Int("timeout", envInt("GIT_SYNC_TIMEOUT", 120), + "the max number of seconds for a complete sync") var flOneTime = flag.Bool("one-time", envBool("GIT_SYNC_ONE_TIME", false), "exit after the initial checkout") var flMaxSyncFailures = flag.Int("max-sync-failures", envInt("GIT_SYNC_MAX_SYNC_FAILURES", 0), @@ -181,7 +184,10 @@ func main() { initialSync := true failCount := 0 for { - if err := syncRepo(*flRepo, *flBranch, *flRev, *flDepth, *flRoot, *flDest); err != nil { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(*flSyncTimeout)) + defer cancel() + err := syncRepo(ctx, *flRepo, *flBranch, *flRev, *flDepth, *flRoot, *flDest) + if err != nil { if initialSync || failCount >= *flMaxSyncFailures { log.Errorf("error syncing repo: %v", err) os.Exit(1) @@ -197,7 +203,7 @@ func main() { if *flOneTime { os.Exit(0) } - if isHash, err := revIsHash(*flRev, *flRoot); err != nil { + if isHash, err := revIsHash(ctx, *flRev, *flRoot); err != nil { log.Errorf("can't tell if rev %s is a git hash, exiting", *flRev) os.Exit(1) } else if isHash { @@ -237,7 +243,7 @@ func sleepForever() { } // updateSymlink atomically swaps the symlink to point at the specified directory and cleans up the previous worktree. -func updateSymlink(gitRoot, link, newDir string) error { +func updateSymlink(ctx context.Context, gitRoot, link, newDir string) error { // Get currently-linked repo directory (to be removed), unless it doesn't exist currentDir, err := filepath.EvalSymlinks(path.Join(gitRoot, link)) if err != nil && !os.IsNotExist(err) { @@ -251,12 +257,12 @@ func updateSymlink(gitRoot, link, newDir string) error { return fmt.Errorf("error converting to relative path: %v", err) } - if _, err := runCommand(gitRoot, "ln", "-snf", newDirRelative, "tmp-link"); err != nil { + if _, err := runCommand(ctx, gitRoot, "ln", "-snf", newDirRelative, "tmp-link"); err != nil { return fmt.Errorf("error creating symlink: %v", err) } log.V(1).Infof("created symlink %s -> %s", "tmp-link", newDirRelative) - if _, err := runCommand(gitRoot, "mv", "-T", "tmp-link", link); err != nil { + if _, err := runCommand(ctx, gitRoot, "mv", "-T", "tmp-link", link); err != nil { return fmt.Errorf("error replacing symlink: %v", err) } log.V(1).Infof("renamed symlink %s to %s", "tmp-link", link) @@ -269,7 +275,7 @@ func updateSymlink(gitRoot, link, newDir string) error { log.V(1).Infof("removed %s", currentDir) - _, err := runCommand(gitRoot, "git", "worktree", "prune") + _, err := runCommand(ctx, gitRoot, "git", "worktree", "prune") if err != nil { return err } @@ -281,17 +287,17 @@ func updateSymlink(gitRoot, link, newDir string) error { } // addWorktreeAndSwap creates a new worktree and calls updateSymlink to swap the symlink to point to the new worktree -func addWorktreeAndSwap(gitRoot, dest, branch, rev, hash string) error { +func addWorktreeAndSwap(ctx context.Context, gitRoot, dest, branch, rev, hash string) error { log.V(0).Infof("syncing to %s (%s)", rev, hash) // Update from the remote. - if _, err := runCommand(gitRoot, "git", "fetch", "--tags", "origin", branch); err != nil { + if _, err := runCommand(ctx, gitRoot, "git", "fetch", "--tags", "origin", branch); err != nil { return err } // Make a worktree for this exact git hash. worktreePath := path.Join(gitRoot, "rev-"+hash) - _, err := runCommand(gitRoot, "git", "worktree", "add", worktreePath, "origin/"+branch) + _, err := runCommand(ctx, gitRoot, "git", "worktree", "add", worktreePath, "origin/"+branch) if err != nil { return err } @@ -311,7 +317,7 @@ func addWorktreeAndSwap(gitRoot, dest, branch, rev, hash string) error { } // Reset the worktree's working copy to the specific rev. - _, err = runCommand(worktreePath, "git", "reset", "--hard", hash) + _, err = runCommand(ctx, worktreePath, "git", "reset", "--hard", hash) if err != nil { return err } @@ -319,22 +325,22 @@ func addWorktreeAndSwap(gitRoot, dest, branch, rev, hash string) error { if *flChmod != 0 { // set file permissions - _, err = runCommand("", "chmod", "-R", strconv.Itoa(*flChmod), worktreePath) + _, err = runCommand(ctx, "", "chmod", "-R", strconv.Itoa(*flChmod), worktreePath) if err != nil { return err } } - return updateSymlink(gitRoot, dest, worktreePath) + return updateSymlink(ctx, gitRoot, dest, worktreePath) } -func cloneRepo(repo, branch, rev string, depth int, gitRoot string) error { +func cloneRepo(ctx context.Context, repo, branch, rev string, depth int, gitRoot string) error { args := []string{"clone", "--no-checkout", "-b", branch} if depth != 0 { args = append(args, "--depth", strconv.Itoa(depth)) } args = append(args, repo, gitRoot) - _, err := runCommand("", "git", args...) + _, err := runCommand(ctx, "", "git", args...) if err != nil { if strings.Contains(err.Error(), "already exists and is not an empty directory") { // Maybe a previous run crashed? Git won't use this dir. @@ -343,7 +349,7 @@ func cloneRepo(repo, branch, rev string, depth int, gitRoot string) error { if err != nil { return err } - _, err = runCommand("", "git", args...) + _, err = runCommand(ctx, "", "git", args...) if err != nil { return err } @@ -356,20 +362,20 @@ func cloneRepo(repo, branch, rev string, depth int, gitRoot string) error { return nil } -func hashForRev(rev, gitRoot string) (string, error) { - output, err := runCommand(gitRoot, "git", "rev-parse", rev) +func hashForRev(ctx context.Context, rev, gitRoot string) (string, error) { + output, err := runCommand(ctx, gitRoot, "git", "rev-parse", rev) if err != nil { return "", err } return strings.Trim(string(output), "\n"), nil } -func revIsHash(rev, gitRoot string) (bool, error) { +func revIsHash(ctx context.Context, rev, gitRoot string) (bool, error) { // If rev is a tag name or HEAD, rev-parse will produce the git hash. If // rev is already a git hash, the output will be the same hash. Of course, a // user could specify "abc" and match "abcdef12345678", so we just do a // prefix match. - output, err := hashForRev(rev, gitRoot) + output, err := hashForRev(ctx, rev, gitRoot) if err != nil { return false, err } @@ -377,25 +383,25 @@ func revIsHash(rev, gitRoot string) (bool, error) { } // syncRepo syncs the branch of a given repository to the destination at the given rev. -func syncRepo(repo, branch, rev string, depth int, gitRoot, dest string) error { +func syncRepo(ctx context.Context, repo, branch, rev string, depth int, gitRoot, dest string) error { target := path.Join(gitRoot, dest) gitRepoPath := path.Join(target, ".git") hash := rev _, err := os.Stat(gitRepoPath) switch { case os.IsNotExist(err): - err = cloneRepo(repo, branch, rev, depth, gitRoot) + err = cloneRepo(ctx, repo, branch, rev, depth, gitRoot) if err != nil { return err } - hash, err = hashForRev(rev, gitRoot) + hash, err = hashForRev(ctx, rev, gitRoot) if err != nil { return err } case err != nil: return fmt.Errorf("error checking if repo exists %q: %v", gitRepoPath, err) default: - local, remote, err := getRevs(target, branch, rev) + local, remote, err := getRevs(ctx, target, branch, rev) if err != nil { return err } @@ -410,13 +416,13 @@ func syncRepo(repo, branch, rev string, depth int, gitRoot, dest string) error { } } - return addWorktreeAndSwap(gitRoot, dest, branch, rev, hash) + return addWorktreeAndSwap(ctx, gitRoot, dest, branch, rev, hash) } // getRevs returns the local and upstream hashes for rev. -func getRevs(localDir, branch, rev string) (string, string, error) { +func getRevs(ctx context.Context, localDir, branch, rev string) (string, string, error) { // Ask git what the exact hash is for rev. - local, err := hashForRev(rev, localDir) + local, err := hashForRev(ctx, rev, localDir) if err != nil { return "", "", err } @@ -430,7 +436,7 @@ func getRevs(localDir, branch, rev string) (string, string, error) { } // Figure out what hash the remote resolves ref to. - remote, err := remoteHashForRef(ref, localDir) + remote, err := remoteHashForRef(ctx, ref, localDir) if err != nil { return "", "", err } @@ -438,8 +444,8 @@ func getRevs(localDir, branch, rev string) (string, string, error) { return local, remote, nil } -func remoteHashForRef(ref, gitRoot string) (string, error) { - output, err := runCommand(gitRoot, "git", "ls-remote", "-q", "origin", ref) +func remoteHashForRef(ctx context.Context, ref, gitRoot string) (string, error) { + output, err := runCommand(ctx, gitRoot, "git", "ls-remote", "-q", "origin", ref) if err != nil { return "", err } @@ -459,14 +465,17 @@ func cmdForLog(command string, args ...string) string { return command + " " + strings.Join(args, " ") } -func runCommand(cwd, command string, args ...string) (string, error) { +func runCommand(ctx context.Context, cwd, command string, args ...string) (string, error) { log.V(5).Infof("run(%q): %s", cwd, cmdForLog(command, args...)) - cmd := exec.Command(command, args...) + cmd := exec.CommandContext(ctx, command, args...) if cwd != "" { cmd.Dir = cwd } output, err := cmd.CombinedOutput() + if ctx.Err() == context.DeadlineExceeded { + return "", fmt.Errorf("command timed out: %v: %q", err, string(output)) + } if err != nil { return "", fmt.Errorf("error running command: %v: %q", err, string(output)) }