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  }