in cmd/git-sync/main.go [244:566]
func main() {
// In case we come up as pid 1, act as init.
if os.Getpid() == 1 {
fmt.Fprintf(os.Stderr, "INFO: detected pid 1, running init handler\n")
code, err := pid1.ReRun()
if err == nil {
os.Exit(code)
}
fmt.Fprintf(os.Stderr, "ERROR: unhandled pid1 error: %v\n", err)
os.Exit(127)
}
setFlagDefaults()
flag.Parse()
log = logging.New(*flRoot, *flErrorFile)
cmdRunner = cmd.NewRunner(log)
if *flVer {
fmt.Println(version.VERSION)
os.Exit(0)
}
if *flRepo == "" {
handleError(true, "ERROR: --repo must be specified")
}
if *flDepth < 0 { // 0 means "no limit"
handleError(true, "ERROR: --depth must be greater than or equal to 0")
}
switch *flSubmodules {
case submodulesRecursive, submodulesShallow, submodulesOff:
default:
handleError(true, "ERROR: --submodules must be one of %q, %q, or %q", submodulesRecursive, submodulesShallow, submodulesOff)
}
if *flRoot == "" {
handleError(true, "ERROR: --root must be specified")
}
if *flDest == "" {
parts := strings.Split(strings.Trim(*flRepo, "/"), "/")
*flDest = parts[len(parts)-1]
}
if !filepath.IsAbs(*flDest) {
*flDest = filepath.Join(*flRoot, *flDest)
}
if *flWait < 0 {
handleError(true, "ERROR: --wait must be greater than or equal to 0")
}
if *flSyncTimeout < 0 {
handleError(true, "ERROR: --timeout must be greater than 0")
}
if *flWebhookURL != "" {
if *flWebhookStatusSuccess < -1 {
handleError(true, "ERROR: --webhook-success-status must be a valid HTTP code or -1")
}
if *flWebhookTimeout < time.Second {
handleError(true, "ERROR: --webhook-timeout must be at least 1s")
}
if *flWebhookBackoff < time.Second {
handleError(true, "ERROR: --webhook-backoff must be at least 1s")
}
}
// Convert deprecated sync-hook-command flag to exechook-command flag
if *flExechookCommand == "" && *flSyncHookCommand != "" {
*flExechookCommand = *flSyncHookCommand
log.Info("--sync-hook-command is deprecated, please use --exechook-command instead")
}
if *flExechookCommand != "" {
if *flExechookTimeout < time.Second {
handleError(true, "ERROR: --exechook-timeout must be at least 1s")
}
if *flExechookBackoff < time.Second {
handleError(true, "ERROR: --exechook-backoff must be at least 1s")
}
}
if _, err := exec.LookPath(*flGitCmd); err != nil {
handleError(false, "ERROR: git executable %q not found: %v", *flGitCmd, err)
}
if *flPassword != "" && *flPasswordFile != "" {
handleError(false, "ERROR: only one of --password and --password-file may be specified")
}
if *flUsername != "" {
if *flPassword == "" && *flPasswordFile == "" {
handleError(true, "ERROR: --password or --password-file must be set when --username is specified")
}
}
if *flSSH {
if *flUsername != "" {
handleError(false, "ERROR: only one of --ssh and --username may be specified")
}
if *flPassword != "" {
handleError(false, "ERROR: only one of --ssh and --password may be specified")
}
if *flPasswordFile != "" {
handleError(false, "ERROR: only one of --ssh and --password-file may be specified")
}
if *flAskPassURL != "" {
handleError(false, "ERROR: only one of --ssh and --askpass-url may be specified")
}
if *flCookieFile {
handleError(false, "ERROR: only one of --ssh and --cookie-file may be specified")
}
if *flSSHKeyFile == "" {
handleError(true, "ERROR: --ssh-key-file must be specified when --ssh is specified")
}
if *flSSHKnownHosts {
if *flSSHKnownHostsFile == "" {
handleError(true, "ERROR: --ssh-known-hosts-file must be specified when --ssh-known-hosts is specified")
}
}
}
if *flAddUser {
if err := addUser(); err != nil {
handleError(false, "ERROR: can't write to /etc/passwd: %v", err)
}
}
// This context is used only for git credentials initialization. There are no long-running operations like
// `git clone`, so initTimeout set to 30 seconds should be enough.
ctx, cancel := context.WithTimeout(context.Background(), initTimeout)
if *flUsername != "" {
if *flPasswordFile != "" {
passwordFileBytes, err := ioutil.ReadFile(*flPasswordFile)
if err != nil {
log.Error(err, "ERROR: can't read password file")
os.Exit(1)
}
*flPassword = string(passwordFileBytes)
}
if err := setupGitAuth(ctx, *flUsername, *flPassword, *flRepo); err != nil {
handleError(false, "ERROR: can't create .netrc file: %v", err)
}
}
if *flSSH {
if err := setupGitSSH(*flSSHKnownHosts); err != nil {
handleError(false, "ERROR: can't configure SSH: %v", err)
}
}
if *flCookieFile {
if err := setupGitCookieFile(ctx); err != nil {
handleError(false, "ERROR: can't set git cookie file: %v", err)
}
}
if *flAskPassURL != "" {
if err := callGitAskPassURL(ctx, *flAskPassURL); err != nil {
askpassCount.WithLabelValues(metricKeyError).Inc()
handleError(false, "ERROR: failed to call ASKPASS callback URL: %v", err)
}
askpassCount.WithLabelValues(metricKeySuccess).Inc()
}
// This needs to be after all other git-related config flags.
if *flGitConfig != "" {
if err := setupExtraGitConfigs(ctx, *flGitConfig); err != nil {
fmt.Fprintf(os.Stderr, "ERROR: can't set additional git configs: %v\n", err)
os.Exit(1)
}
}
// The scope of the initialization context ends here, so we call cancel to release resources associated with it.
cancel()
if *flHTTPBind != "" {
ln, err := net.Listen("tcp", *flHTTPBind)
if err != nil {
handleError(false, "ERROR: unable to bind HTTP endpoint: %v", err)
}
mux := http.NewServeMux()
go func() {
if *flHTTPMetrics {
mux.Handle("/metrics", promhttp.Handler())
}
if *flHTTPprof {
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
}
// This is a dumb liveliness check endpoint. Currently this checks
// nothing and will always return 200 if the process is live.
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if !getRepoReady() {
http.Error(w, "repo is not ready", http.StatusServiceUnavailable)
}
// Otherwise success
})
http.Serve(ln, mux)
}()
}
// From here on, output goes through logging.
log.V(0).Info("starting up", "pid", os.Getpid(), "args", os.Args)
// Startup webhooks goroutine
var webhookRunner *hook.HookRunner
if *flWebhookURL != "" {
webhook := hook.NewWebhook(
*flWebhookURL,
*flWebhookMethod,
*flWebhookStatusSuccess,
*flWebhookTimeout,
log,
)
webhookRunner = hook.NewHookRunner(
webhook,
*flWebhookBackoff,
hook.NewHookData(),
log,
*flOneTime,
)
go webhookRunner.Run(context.Background())
}
// Startup exechooks goroutine
var exechookRunner *hook.HookRunner
if *flExechookCommand != "" {
exechook := hook.NewExechook(
cmd.NewRunner(log),
*flExechookCommand,
*flRoot,
[]string{},
*flExechookTimeout,
log,
)
exechookRunner = hook.NewHookRunner(
exechook,
*flExechookBackoff,
hook.NewHookData(),
log,
*flOneTime,
)
go exechookRunner.Run(context.Background())
}
initialSync := true
failCount := 0
for {
start := time.Now()
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(*flSyncTimeout))
if changed, hash, err := syncRepo(ctx, *flRepo, *flBranch, *flRev, *flDepth, *flRoot, *flDest, *flAskPassURL, *flSubmodules); err != nil {
updateSyncMetrics(metricKeyError, start)
if *flMaxSyncFailures != -1 && failCount >= *flMaxSyncFailures {
// Exit after too many retries, maybe the error is not recoverable.
log.Error(err, "too many failures, aborting", "failCount", failCount)
os.Exit(1)
}
failCount++
log.Error(err, "unexpected error syncing repo, will retry")
log.V(0).Info("waiting before retrying", "waitTime", waitTime(*flWait))
cancel()
time.Sleep(waitTime(*flWait))
continue
} else if changed {
if webhookRunner != nil {
webhookRunner.Send(hash)
}
if exechookRunner != nil {
exechookRunner.Send(hash)
}
updateSyncMetrics(metricKeySuccess, start)
} else {
updateSyncMetrics(metricKeyNoOp, start)
}
if initialSync {
// Determine if git-sync should terminate for one of several reasons
if *flOneTime {
// Wait for hooks to complete at least once, if not nil, before
// checking whether to stop program.
// 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
}
}
if webhookRunner != nil {
if err := webhookRunner.WaitForCompletion(); err != nil {
exitCode = 1
}
}
log.DeleteErrorFile()
os.Exit(exitCode)
}
if isHash, err := revIsHash(ctx, *flRev, *flRoot); err != nil {
log.Error(err, "can't tell if rev is a git hash, exiting", "rev", *flRev)
os.Exit(1)
} else if isHash {
log.V(0).Info("rev appears to be a git hash, no further sync needed", "rev", *flRev)
log.DeleteErrorFile()
sleepForever()
}
initialSync = false
}
failCount = 0
log.DeleteErrorFile()
log.V(1).Info("next sync", "wait_time", waitTime(*flWait))
cancel()
time.Sleep(waitTime(*flWait))
}
}