Add a main struct, part 1

Start the process of encapsulating most of the flags and not using them
as global variables.  This commit JUST does the git command flag, which
is now only accessed from main()
This commit is contained in:
Tim Hockin 2020-11-07 14:48:36 -08:00
parent 073b007ebd
commit 2dd4705c1b
1 changed files with 61 additions and 45 deletions

View File

@ -249,6 +249,11 @@ func setGlogFlags() {
vFlag.Value.Set(strconv.Itoa(*flVerbose)) vFlag.Value.Set(strconv.Itoa(*flVerbose))
} }
// repoSync represents the remote repo and the local sync of it.
type repoSync struct {
cmd string // the git command to run
}
func main() { func main() {
// In case we come up as pid 1, act as init. // In case we come up as pid 1, act as init.
if os.Getpid() == 1 { if os.Getpid() == 1 {
@ -425,12 +430,17 @@ func main() {
} }
} }
// Capture the various git parameters.
git := &repoSync{
cmd: *flGitCmd,
}
// This context is used only for git credentials initialization. There are no long-running operations like // This context is used only for git credentials initialization. There are no long-running operations like
// `git clone`, so hopefully 30 seconds will be enough. // `git clone`, so hopefully 30 seconds will be enough.
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
if *flUsername != "" && *flPassword != "" { if *flUsername != "" && *flPassword != "" {
if err := setupGitAuth(ctx, *flUsername, *flPassword, *flRepo); err != nil { if err := git.SetupAuth(ctx, *flUsername, *flPassword, *flRepo); err != nil {
log.Error(err, "ERROR: can't set up git auth") log.Error(err, "ERROR: can't set up git auth")
os.Exit(1) os.Exit(1)
} }
@ -444,14 +454,14 @@ func main() {
} }
if *flCookieFile { if *flCookieFile {
if err := setupGitCookieFile(ctx); err != nil { if err := git.SetupCookieFile(ctx); err != nil {
log.Error(err, "ERROR: can't set up git cookie file") log.Error(err, "ERROR: can't set up git cookie file")
os.Exit(1) os.Exit(1)
} }
} }
if *flAskPassURL != "" { if *flAskPassURL != "" {
if err := callGitAskPassURL(ctx, *flAskPassURL); err != nil { if err := git.CallAskPassURL(ctx, *flAskPassURL); err != nil {
askpassCount.WithLabelValues(metricKeyError).Inc() askpassCount.WithLabelValues(metricKeyError).Inc()
log.Error(err, "ERROR: failed to call ASKPASS callback URL", "url", *flAskPassURL) log.Error(err, "ERROR: failed to call ASKPASS callback URL", "url", *flAskPassURL)
os.Exit(1) os.Exit(1)
@ -516,7 +526,7 @@ func main() {
for { for {
start := time.Now() start := time.Now()
ctx, cancel := context.WithTimeout(context.Background(), *flSyncTimeout) ctx, cancel := context.WithTimeout(context.Background(), *flSyncTimeout)
if changed, hash, err := syncRepo(ctx, *flRepo, *flBranch, *flRev, *flDepth, absRoot, *flLink, *flAskPassURL, *flSubmodules); err != nil { if changed, hash, err := git.SyncRepo(ctx, *flRepo, *flBranch, *flRev, *flDepth, absRoot, *flLink, *flAskPassURL, *flSubmodules); err != nil {
updateSyncMetrics(metricKeyError, start) updateSyncMetrics(metricKeyError, start)
if *flMaxSyncFailures != -1 && failCount >= *flMaxSyncFailures { if *flMaxSyncFailures != -1 && failCount >= *flMaxSyncFailures {
// Exit after too many retries, maybe the error is not recoverable. // Exit after too many retries, maybe the error is not recoverable.
@ -543,7 +553,7 @@ func main() {
if *flOneTime { if *flOneTime {
os.Exit(0) os.Exit(0)
} }
if isHash, err := revIsHash(ctx, *flRev, absRoot); err != nil { if isHash, err := git.RevIsHash(ctx, *flRev, absRoot); err != nil {
log.Error(err, "can't tell if rev is a git hash, exiting", "rev", *flRev) log.Error(err, "can't tell if rev is a git hash, exiting", "rev", *flRev)
os.Exit(1) os.Exit(1)
} else if isHash { } else if isHash {
@ -657,8 +667,8 @@ func setRepoReady() {
repoReady = true repoReady = true
} }
// addWorktreeAndSwap creates a new worktree and calls updateSymlink to swap the symlink to point to the new worktree // AddWorktreeAndSwap creates a new worktree and calls updateSymlink to swap the symlink to point to the new worktree
func addWorktreeAndSwap(ctx context.Context, gitRoot, link, branch, rev string, depth int, hash string, submoduleMode string) error { func (git *repoSync) AddWorktreeAndSwap(ctx context.Context, gitRoot, link, branch, rev string, depth int, hash string, submoduleMode string) error {
log.V(0).Info("syncing git", "rev", rev, "hash", hash) log.V(0).Info("syncing git", "rev", rev, "hash", hash)
args := []string{"fetch", "-f", "--tags"} args := []string{"fetch", "-f", "--tags"}
@ -668,18 +678,18 @@ func addWorktreeAndSwap(ctx context.Context, gitRoot, link, branch, rev string,
args = append(args, "origin", branch) args = append(args, "origin", branch)
// Update from the remote. // Update from the remote.
if _, err := runCommand(ctx, gitRoot, *flGitCmd, args...); err != nil { if _, err := runCommand(ctx, gitRoot, git.cmd, args...); err != nil {
return err return err
} }
// GC clone // GC clone
if _, err := runCommand(ctx, gitRoot, *flGitCmd, "gc", "--prune=all"); err != nil { if _, err := runCommand(ctx, gitRoot, git.cmd, "gc", "--prune=all"); err != nil {
return err return err
} }
// Make a worktree for this exact git hash. // Make a worktree for this exact git hash.
worktreePath := filepath.Join(gitRoot, "rev-"+hash) worktreePath := filepath.Join(gitRoot, "rev-"+hash)
_, err := runCommand(ctx, gitRoot, *flGitCmd, "worktree", "add", worktreePath, "origin/"+branch) _, err := runCommand(ctx, gitRoot, git.cmd, "worktree", "add", worktreePath, "origin/"+branch)
log.V(0).Info("adding worktree", "path", worktreePath, "branch", fmt.Sprintf("origin/%s", branch)) log.V(0).Info("adding worktree", "path", worktreePath, "branch", fmt.Sprintf("origin/%s", branch))
if err != nil { if err != nil {
return err return err
@ -699,7 +709,7 @@ func addWorktreeAndSwap(ctx context.Context, gitRoot, link, branch, rev string,
} }
// Reset the worktree's working copy to the specific rev. // Reset the worktree's working copy to the specific rev.
_, err = runCommand(ctx, worktreePath, *flGitCmd, "reset", "--hard", hash) _, err = runCommand(ctx, worktreePath, git.cmd, "reset", "--hard", hash)
if err != nil { if err != nil {
return err return err
} }
@ -716,7 +726,7 @@ func addWorktreeAndSwap(ctx context.Context, gitRoot, link, branch, rev string,
if depth != 0 { if depth != 0 {
submodulesArgs = append(submodulesArgs, "--depth", strconv.Itoa(depth)) submodulesArgs = append(submodulesArgs, "--depth", strconv.Itoa(depth))
} }
_, err = runCommand(ctx, worktreePath, *flGitCmd, submodulesArgs...) _, err = runCommand(ctx, worktreePath, git.cmd, submodulesArgs...)
if err != nil { if err != nil {
return err return err
} }
@ -753,7 +763,7 @@ func addWorktreeAndSwap(ctx context.Context, gitRoot, link, branch, rev string,
if err := os.RemoveAll(oldWorktree); err != nil { if err := os.RemoveAll(oldWorktree); err != nil {
return fmt.Errorf("error removing directory: %v", err) return fmt.Errorf("error removing directory: %v", err)
} }
if _, err := runCommand(ctx, gitRoot, *flGitCmd, "worktree", "prune"); err != nil { if _, err := runCommand(ctx, gitRoot, git.cmd, "worktree", "prune"); err != nil {
return err return err
} }
} }
@ -761,7 +771,8 @@ func addWorktreeAndSwap(ctx context.Context, gitRoot, link, branch, rev string,
return nil return nil
} }
func cloneRepo(ctx context.Context, repo, branch, rev string, depth int, gitRoot string) error { // CloneRepo does an initial clone of the git repo.
func (git *repoSync) CloneRepo(ctx context.Context, repo, branch, rev string, depth int, gitRoot string) error {
args := []string{"clone", "--no-checkout", "-b", branch} args := []string{"clone", "--no-checkout", "-b", branch}
if depth != 0 { if depth != 0 {
args = append(args, "--depth", strconv.Itoa(depth)) args = append(args, "--depth", strconv.Itoa(depth))
@ -769,7 +780,7 @@ func cloneRepo(ctx context.Context, repo, branch, rev string, depth int, gitRoot
args = append(args, repo, gitRoot) args = append(args, repo, gitRoot)
log.V(0).Info("cloning repo", "origin", repo, "path", gitRoot) log.V(0).Info("cloning repo", "origin", repo, "path", gitRoot)
_, err := runCommand(ctx, "", *flGitCmd, args...) _, err := runCommand(ctx, "", git.cmd, args...)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "already exists and is not an empty directory") { if strings.Contains(err.Error(), "already exists and is not an empty directory") {
// Maybe a previous run crashed? Git won't use this dir. // Maybe a previous run crashed? Git won't use this dir.
@ -778,7 +789,7 @@ func cloneRepo(ctx context.Context, repo, branch, rev string, depth int, gitRoot
if err != nil { if err != nil {
return err return err
} }
_, err = runCommand(ctx, "", *flGitCmd, args...) _, err = runCommand(ctx, "", git.cmd, args...)
if err != nil { if err != nil {
return err return err
} }
@ -790,18 +801,18 @@ func cloneRepo(ctx context.Context, repo, branch, rev string, depth int, gitRoot
return nil return nil
} }
// localHashForRev returns the locally known hash for a given rev. // LocalHashForRev returns the locally known hash for a given rev.
func localHashForRev(ctx context.Context, rev, gitRoot string) (string, error) { func (git *repoSync) LocalHashForRev(ctx context.Context, rev, gitRoot string) (string, error) {
output, err := runCommand(ctx, gitRoot, *flGitCmd, "rev-parse", rev) output, err := runCommand(ctx, gitRoot, git.cmd, "rev-parse", rev)
if err != nil { if err != nil {
return "", err return "", err
} }
return strings.Trim(string(output), "\n"), nil return strings.Trim(string(output), "\n"), nil
} }
// remoteHashForRef returns the upstream hash for a given ref. // RemoteHashForRef returns the upstream hash for a given ref.
func remoteHashForRef(ctx context.Context, ref, gitRoot string) (string, error) { func (git *repoSync) RemoteHashForRef(ctx context.Context, ref, gitRoot string) (string, error) {
output, err := runCommand(ctx, gitRoot, *flGitCmd, "ls-remote", "-q", "origin", ref) output, err := runCommand(ctx, gitRoot, git.cmd, "ls-remote", "-q", "origin", ref)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -809,9 +820,9 @@ func remoteHashForRef(ctx context.Context, ref, gitRoot string) (string, error)
return parts[0], nil return parts[0], nil
} }
func revIsHash(ctx context.Context, rev, gitRoot string) (bool, error) { func (git *repoSync) RevIsHash(ctx context.Context, rev, gitRoot string) (bool, error) {
// If git doesn't identify rev as a commit, we're done. // If git doesn't identify rev as a commit, we're done.
output, err := runCommand(ctx, gitRoot, *flGitCmd, "cat-file", "-t", rev) output, err := runCommand(ctx, gitRoot, git.cmd, "cat-file", "-t", rev)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -824,20 +835,20 @@ func revIsHash(ctx context.Context, rev, gitRoot string) (bool, error) {
// hash, the output will be the same hash as the input. Of course, a user // hash, the output will be the same hash as the input. Of course, a user
// could specify "abc" and match "abcdef12345678", so we just do a prefix // could specify "abc" and match "abcdef12345678", so we just do a prefix
// match. // match.
output, err = localHashForRev(ctx, rev, gitRoot) output, err = git.LocalHashForRev(ctx, rev, gitRoot)
if err != nil { if err != nil {
return false, err return false, err
} }
return strings.HasPrefix(output, rev), nil return strings.HasPrefix(output, rev), nil
} }
// syncRepo syncs the branch of a given repository to the link at the given rev. // SyncRepo syncs the branch of a given repository to the link at the given rev.
// returns (1) whether a change occured, (2) the new hash, and (3) an error if one happened // returns (1) whether a change occured, (2) the new hash, and (3) an error if one happened
func syncRepo(ctx context.Context, repo, branch, rev string, depth int, gitRoot, link string, authURL string, submoduleMode string) (bool, string, error) { func (git *repoSync) SyncRepo(ctx context.Context, repo, branch, rev string, depth int, gitRoot, link string, authURL string, submoduleMode string) (bool, string, error) {
if authURL != "" { if authURL != "" {
// For ASKPASS Callback URL, the credentials behind is dynamic, it needs to be // For ASKPASS Callback URL, the credentials behind is dynamic, it needs to be
// re-fetched each time. // re-fetched each time.
if err := callGitAskPassURL(ctx, authURL); err != nil { if err := git.CallAskPassURL(ctx, authURL); err != nil {
askpassCount.WithLabelValues(metricKeyError).Inc() askpassCount.WithLabelValues(metricKeyError).Inc()
return false, "", fmt.Errorf("failed to call GIT_ASKPASS_URL: %v", err) return false, "", fmt.Errorf("failed to call GIT_ASKPASS_URL: %v", err)
} }
@ -851,11 +862,11 @@ func syncRepo(ctx context.Context, repo, branch, rev string, depth int, gitRoot,
switch { switch {
case os.IsNotExist(err): case os.IsNotExist(err):
// First time. Just clone it and get the hash. // First time. Just clone it and get the hash.
err = cloneRepo(ctx, repo, branch, rev, depth, gitRoot) err = git.CloneRepo(ctx, repo, branch, rev, depth, gitRoot)
if err != nil { if err != nil {
return false, "", err return false, "", err
} }
hash, err = localHashForRev(ctx, rev, gitRoot) hash, err = git.LocalHashForRev(ctx, rev, gitRoot)
if err != nil { if err != nil {
return false, "", err return false, "", err
} }
@ -863,7 +874,7 @@ func syncRepo(ctx context.Context, repo, branch, rev string, depth int, gitRoot,
return false, "", fmt.Errorf("error checking if repo exists %q: %v", gitRepoPath, err) return false, "", fmt.Errorf("error checking if repo exists %q: %v", gitRepoPath, err)
default: default:
// Not the first time. Figure out if the ref has changed. // Not the first time. Figure out if the ref has changed.
local, remote, err := getRevs(ctx, target, branch, rev) local, remote, err := git.GetRevs(ctx, target, branch, rev)
if err != nil { if err != nil {
return false, "", err return false, "", err
} }
@ -875,13 +886,13 @@ func syncRepo(ctx context.Context, repo, branch, rev string, depth int, gitRoot,
hash = remote hash = remote
} }
return true, hash, addWorktreeAndSwap(ctx, gitRoot, link, branch, rev, depth, hash, submoduleMode) return true, hash, git.AddWorktreeAndSwap(ctx, gitRoot, link, branch, rev, depth, hash, submoduleMode)
} }
// getRevs returns the local and upstream hashes for rev. // GetRevs returns the local and upstream hashes for rev.
func getRevs(ctx context.Context, localDir, branch, rev string) (string, string, error) { func (git *repoSync) GetRevs(ctx context.Context, localDir, branch, rev string) (string, string, error) {
// Ask git what the exact hash is for rev. // Ask git what the exact hash is for rev.
local, err := localHashForRev(ctx, rev, localDir) local, err := git.LocalHashForRev(ctx, rev, localDir)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
@ -895,7 +906,7 @@ func getRevs(ctx context.Context, localDir, branch, rev string) (string, string,
} }
// Figure out what hash the remote resolves ref to. // Figure out what hash the remote resolves ref to.
remote, err := remoteHashForRef(ctx, ref, localDir) remote, err := git.RemoteHashForRef(ctx, ref, localDir)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
@ -947,16 +958,18 @@ func runCommandWithStdin(ctx context.Context, cwd, stdin, command string, args .
return stdout, nil return stdout, nil
} }
func setupGitAuth(ctx context.Context, username, password, gitURL string) error { // SetupAuth configures the local git repo to use a username and password when
// accessing the repo at gitURL.
func (git *repoSync) SetupAuth(ctx context.Context, username, password, gitURL string) error {
log.V(1).Info("setting up git credential store") log.V(1).Info("setting up git credential store")
_, err := runCommand(ctx, "", *flGitCmd, "config", "--global", "credential.helper", "store") _, err := runCommand(ctx, "", git.cmd, "config", "--global", "credential.helper", "store")
if err != nil { if err != nil {
return fmt.Errorf("can't configure git credential helper: %w", err) return fmt.Errorf("can't configure git credential helper: %w", err)
} }
creds := fmt.Sprintf("url=%v\nusername=%v\npassword=%v\n", gitURL, username, password) creds := fmt.Sprintf("url=%v\nusername=%v\npassword=%v\n", gitURL, username, password)
_, err = runCommandWithStdin(ctx, "", creds, *flGitCmd, "credential", "approve") _, err = runCommandWithStdin(ctx, "", creds, git.cmd, "credential", "approve")
if err != nil { if err != nil {
return fmt.Errorf("can't configure git credentials: %w", err) return fmt.Errorf("can't configure git credentials: %w", err)
} }
@ -993,7 +1006,7 @@ func setupGitSSH(setupKnownHosts bool) error {
return nil return nil
} }
func setupGitCookieFile(ctx context.Context) error { func (git *repoSync) SetupCookieFile(ctx context.Context) error {
log.V(1).Info("configuring git cookie file") log.V(1).Info("configuring git cookie file")
var pathToCookieFile = "/etc/git-secret/cookie_file" var pathToCookieFile = "/etc/git-secret/cookie_file"
@ -1004,18 +1017,21 @@ func setupGitCookieFile(ctx context.Context) error {
} }
if _, err = runCommand(ctx, "", if _, err = runCommand(ctx, "",
*flGitCmd, "config", "--global", "http.cookiefile", pathToCookieFile); err != nil { git.cmd, "config", "--global", "http.cookiefile", pathToCookieFile); err != nil {
return fmt.Errorf("can't configure git cookiefile: %w", err) return fmt.Errorf("can't configure git cookiefile: %w", err)
} }
return nil return nil
} }
// CallAskPassURL consults the specified URL looking for git credentials in the
// response.
//
// The expected ASKPASS callback output are below, // The expected ASKPASS callback output are below,
// see https://git-scm.com/docs/gitcredentials for more examples: // see https://git-scm.com/docs/gitcredentials for more examples:
// username=xxx@example.com // username=xxx@example.com
// password=ya29.xxxyyyzzz // password=ya29.xxxyyyzzz
func callGitAskPassURL(ctx context.Context, url string) error { func (git *repoSync) CallAskPassURL(ctx context.Context, url string) error {
log.V(1).Info("calling GIT_ASKPASS URL to get credentials") log.V(1).Info("calling GIT_ASKPASS URL to get credentials")
var netClient = &http.Client{ var netClient = &http.Client{
@ -1056,7 +1072,7 @@ func callGitAskPassURL(ctx context.Context, url string) error {
} }
} }
if err := setupGitAuth(ctx, username, password, *flRepo); err != nil { if err := git.SetupAuth(ctx, username, password, *flRepo); err != nil {
return err return err
} }