github.com/Cloud-Foundations/Dominator@v0.3.4/lib/repowatch/impl.go (about) 1 package repowatch 2 3 import ( 4 "errors" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "os/exec" 9 "path/filepath" 10 "strings" 11 "time" 12 13 "github.com/Cloud-Foundations/Dominator/lib/fsutil" 14 "github.com/Cloud-Foundations/Dominator/lib/log" 15 "github.com/Cloud-Foundations/tricorder/go/tricorder" 16 "github.com/Cloud-Foundations/tricorder/go/tricorder/units" 17 ) 18 19 type gitMetricsType struct { 20 lastAttemptedPullTime time.Time 21 lastCommitId string 22 lastSuccessfulPullTime time.Time 23 lastNotificationTime time.Time 24 latencyDistribution *tricorder.CumulativeDistribution 25 } 26 27 func checkDirectory(directory string) error { 28 if fi, err := os.Stat(directory); err != nil { 29 return err 30 } else if !fi.IsDir() { 31 return fmt.Errorf("not a directory: %s", directory) 32 } 33 return nil 34 } 35 36 func gitCommand(repositoryDirectory string, command ...string) error { 37 cmd := exec.Command("git", command...) 38 cmd.Dir = repositoryDirectory 39 if output, err := cmd.CombinedOutput(); err != nil { 40 return errors.New(err.Error() + ": " + string(output)) 41 } 42 return nil 43 } 44 45 func gitPull(repositoryDirectory string, 46 metrics *gitMetricsType) (string, error) { 47 metrics.lastAttemptedPullTime = time.Now() 48 if err := gitCommand(repositoryDirectory, "pull"); err != nil { 49 return "", err 50 } 51 metrics.lastSuccessfulPullTime = time.Now() 52 metrics.latencyDistribution.Add( 53 metrics.lastSuccessfulPullTime.Sub(metrics.lastAttemptedPullTime)) 54 return readLatestCommitId(repositoryDirectory) 55 } 56 57 func readLatestCommitId(repositoryDirectory string) (string, error) { 58 commitId, err := ioutil.ReadFile( 59 filepath.Join(repositoryDirectory, ".git/refs/heads/master")) 60 if err != nil { 61 return "", err 62 } 63 return strings.TrimSpace(string(commitId)), nil 64 } 65 66 func setupGitRepository(remoteURL, localDirectory, awsSecretId string, 67 metrics *gitMetricsType, logger log.DebugLogger) (string, error) { 68 if err := os.MkdirAll(localDirectory, fsutil.DirPerms); err != nil { 69 return "", err 70 } 71 gitSubdir := filepath.Join(localDirectory, ".git") 72 if _, err := os.Stat(gitSubdir); err != nil { 73 if !os.IsNotExist(err) { 74 return "", err 75 } 76 metrics.lastAttemptedPullTime = time.Now() 77 if err := awsGetKey(awsSecretId, logger); err != nil { 78 return "", err 79 } 80 err := gitCommand(localDirectory, "clone", remoteURL, ".") 81 if err != nil { 82 return "", err 83 } 84 metrics.lastSuccessfulPullTime = time.Now() 85 return readLatestCommitId(localDirectory) 86 } else { 87 go func() { 88 for { 89 if err := awsGetKey(awsSecretId, logger); err != nil { 90 logger.Println(err) 91 time.Sleep(time.Minute * 5) 92 } else { 93 return 94 } 95 } 96 }() 97 // Try to be as fresh as possible. 98 if commitId, err := gitPull(localDirectory, metrics); err != nil { 99 logger.Println(err) 100 return readLatestCommitId(localDirectory) 101 } else { 102 return commitId, nil 103 } 104 } 105 } 106 107 func watch(config Config, metricDirectory string, 108 logger log.DebugLogger) (<-chan string, error) { 109 if config.Branch != "" && config.Branch != "master" { 110 return nil, errors.New("non-master branch not supported") 111 } 112 if config.CheckInterval < time.Second { 113 config.CheckInterval = time.Second 114 } 115 if config.RepositoryURL == "" { 116 return watchLocal(config.LocalRepositoryDirectory, config.CheckInterval, 117 metricDirectory, logger) 118 } 119 return watchGit(config.RepositoryURL, config.LocalRepositoryDirectory, 120 config.AwsSecretId, config.CheckInterval, metricDirectory, logger) 121 } 122 123 func watchGit(remoteURL, localDirectory, awsSecretId string, 124 checkInterval time.Duration, metricDirectory string, 125 logger log.DebugLogger) (<-chan string, error) { 126 notificationChannel := make(chan string, 1) 127 metrics := &gitMetricsType{ 128 latencyDistribution: tricorder.NewGeometricBucketer(1, 1e5). 129 NewCumulativeDistribution(), 130 } 131 err := tricorder.RegisterMetric(filepath.Join(metricDirectory, 132 "git-pull-latency"), metrics.latencyDistribution, 133 units.Millisecond, "latency of git pull calls") 134 if err != nil { 135 return nil, err 136 } 137 metrics.lastCommitId, err = setupGitRepository(remoteURL, localDirectory, 138 awsSecretId, metrics, logger) 139 if err != nil { 140 return nil, err 141 } 142 err = tricorder.RegisterMetric(filepath.Join(metricDirectory, 143 "last-attempted-git-pull-time"), &metrics.lastAttemptedPullTime, 144 units.None, "time of last attempted git pull") 145 if err != nil { 146 return nil, err 147 } 148 err = tricorder.RegisterMetric(filepath.Join(metricDirectory, 149 "last-commit-id"), &metrics.lastCommitId, 150 units.None, "commit ID in master branch in last successful git pull") 151 if err != nil { 152 return nil, err 153 } 154 err = tricorder.RegisterMetric(filepath.Join(metricDirectory, 155 "last-successful-git-pull-time"), &metrics.lastSuccessfulPullTime, 156 units.None, "time of last successful git pull") 157 if err != nil { 158 return nil, err 159 } 160 err = tricorder.RegisterMetric(filepath.Join(metricDirectory, 161 "last-notification-time"), &metrics.lastNotificationTime, units.None, 162 "time of last git change notification") 163 if err != nil { 164 return nil, err 165 } 166 metrics.lastNotificationTime = time.Now() 167 notificationChannel <- localDirectory 168 go watchGitLoop(localDirectory, checkInterval, metrics, notificationChannel, 169 logger) 170 return notificationChannel, nil 171 } 172 173 func watchGitLoop(directory string, checkInterval time.Duration, 174 metrics *gitMetricsType, notificationChannel chan<- string, 175 logger log.DebugLogger) { 176 for { 177 time.Sleep(checkInterval) 178 if commitId, err := gitPull(directory, metrics); err != nil { 179 logger.Println(err) 180 } else if commitId != metrics.lastCommitId { 181 metrics.lastCommitId = commitId 182 metrics.lastNotificationTime = time.Now() 183 notificationChannel <- directory 184 } 185 } 186 } 187 188 func watchLocal(directory string, checkInterval time.Duration, 189 metricDirectory string, logger log.DebugLogger) (<-chan string, error) { 190 if err := checkDirectory(directory); err != nil { 191 return nil, err 192 } 193 var lastNotificationTime time.Time 194 err := tricorder.RegisterMetric(filepath.Join(metricDirectory, 195 "last-notification-time"), &lastNotificationTime, units.None, 196 "time of last notification") 197 if err != nil { 198 return nil, err 199 } 200 notificationChannel := make(chan string, 1) 201 go watchLocalLoop(directory, checkInterval, &lastNotificationTime, 202 notificationChannel, logger) 203 return notificationChannel, nil 204 } 205 206 func watchLocalLoop(directory string, checkInterval time.Duration, 207 lastNotificationTime *time.Time, notificationChannel chan<- string, 208 logger log.DebugLogger) { 209 for ; ; time.Sleep(checkInterval) { 210 if err := checkDirectory(directory); err != nil { 211 logger.Println(err) 212 } else { 213 *lastNotificationTime = time.Now() 214 notificationChannel <- directory 215 } 216 } 217 }