From 8ca9eaae2345b5e83b7c5b7a40e1a5b4981ceebe Mon Sep 17 00:00:00 2001 From: Mihai Moisa Date: Fri, 16 Jun 2017 09:30:31 +0300 Subject: [PATCH] Updated the consumer and producer libraries. Reworked the health and gtg endpoints. --- Dockerfile | 1 - circle.yml | 2 +- healthcheck.go | 126 ++++++++++++++--------------- healthchecks_test.go | 186 ++++++++++++++++++++++--------------------- main.go | 41 +++++----- mapper.go | 3 +- vendor/vendor.json | 50 ++++++------ 7 files changed, 200 insertions(+), 209 deletions(-) diff --git a/Dockerfile b/Dockerfile index de846f4..f599ea8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,6 @@ RUN apk --no-cache --virtual .build-dependencies add git \ && echo "Fetching dependencies..." \ && go get -u github.com/kardianos/govendor \ && $GOPATH/bin/govendor sync \ - && go get -v \ && go build -ldflags="${LDFLAGS}" \ && mv ${PROJECT} /${PROJECT} \ && apk del .build-dependencies \ diff --git a/circle.yml b/circle.yml index 59e5431..b38fdd3 100644 --- a/circle.yml +++ b/circle.yml @@ -8,7 +8,7 @@ machine: checkout: post: - mkdir -p "${PROJECT_PARENT_PATH}" - - rsync -avC "${HOME}/${CIRCLE_PROJECT_REPONAME}/" "${PROJECT_PATH}" + - ln -sf "${HOME}/${CIRCLE_PROJECT_REPONAME}/" "${PROJECT_PATH}" dependencies: pre: diff --git a/healthcheck.go b/healthcheck.go index 666d420..2adbf03 100644 --- a/healthcheck.go +++ b/healthcheck.go @@ -1,100 +1,90 @@ package main import ( - "errors" - "fmt" - health "github.com/Financial-Times/go-fthealth/v1_1" + "net/http" + "time" + + fthealth "github.com/Financial-Times/go-fthealth/v1_1" "github.com/Financial-Times/message-queue-go-producer/producer" "github.com/Financial-Times/message-queue-gonsumer/consumer" "github.com/Financial-Times/service-status-go/gtg" - "net/http" ) -const healthPath = "/__health" - -type healthService struct { - config *healthConfig - checks []health.Check -} +const requestTimeout = 4500 -type healthConfig struct { +type HealthCheck struct { + consumer consumer.MessageConsumer + producer producer.MessageProducer appSystemCode string appName string - port string - httpCl *http.Client - consumerConf consumer.QueueConfig - producerConf producer.MessageProducerConfig panicGuide string } -func newHealthService(config *healthConfig) *healthService { - service := &healthService{config: config} - service.checks = []health.Check{ - service.queueCheck(), +func newHealthCheck(producerConf *producer.MessageProducerConfig, consumerConf *consumer.QueueConfig, appName, appSystemCode, panicGuide string) *HealthCheck { + httpClient := &http.Client{Timeout: requestTimeout * time.Millisecond} + p := producer.NewMessageProducerWithHTTPClient(*producerConf, httpClient) + c := consumer.NewConsumer(*consumerConf, func(m consumer.Message) {}, httpClient) + return &HealthCheck{ + consumer: c, + producer: p, + appName: appName, + appSystemCode: appSystemCode, + panicGuide: panicGuide, } - return service } -func (service *healthService) queueCheck() health.Check { - return health.Check{ - ID: "message-queue-proxy-reachable", - BusinessImpact: "Related content from published Next videos will not be processed, clients will not see them within content.", - Name: "Message Queue Proxy Reachable", - PanicGuide: service.config.panicGuide, - Severity: 1, - TechnicalSummary: "Message queue proxy is not reachable/healthy", - Checker: service.checkAggregateMessageQueueProxiesReachable, +func (h *HealthCheck) Health() func(w http.ResponseWriter, r *http.Request) { + checks := []fthealth.Check{h.readQueueCheck(), h.writeQueueCheck()} + hc := fthealth.HealthCheck{ + SystemCode: h.appSystemCode, + Name: h.appName, + Description: serviceDescription, + Checks: checks, } + return fthealth.Handler(hc) } -func (service *healthService) checkAggregateMessageQueueProxiesReachable() (string, error) { - var errMsg string - - err := service.checkMessageQueueProxyReachable(service.config.producerConf.Addr, service.config.producerConf.Topic, service.config.producerConf.Authorization, service.config.producerConf.Queue) - if err != nil { - return err.Error(), fmt.Errorf("Health check for queue address %s, topic %s failed. Error: %s", service.config.producerConf.Addr, service.config.producerConf.Topic, err.Error()) +func (h *HealthCheck) readQueueCheck() fthealth.Check { + return fthealth.Check{ + ID: "read-message-queue-proxy-reachable", + Name: "Read Message Queue Proxy Reachable", + Severity: 1, + BusinessImpact: "Related content from published Next videos will not be processed, clients will not see them within content.", + TechnicalSummary: "Read message queue proxy is not reachable/healthy", + PanicGuide: h.panicGuide, + Checker: h.consumer.ConnectivityCheck, } +} - for i := 0; i < len(service.config.consumerConf.Addrs); i++ { - err := service.checkMessageQueueProxyReachable(service.config.consumerConf.Addrs[i], service.config.consumerConf.Topic, service.config.consumerConf.AuthorizationKey, service.config.consumerConf.Queue) - if err == nil { - return "Ok", nil - } - errMsg = errMsg + fmt.Sprintf("Health check for queue address %s, topic %s failed. Error: %s", service.config.consumerConf.Addrs[i], service.config.consumerConf.Topic, err.Error()) +func (h *HealthCheck) writeQueueCheck() fthealth.Check { + return fthealth.Check{ + ID: "write-message-queue-proxy-reachable", + Name: "Write Message Queue Proxy Reachable", + Severity: 1, + BusinessImpact: "Related content from published Next videos will not be processed, clients will not see them within content.", + TechnicalSummary: "Write message queue proxy is not reachable/healthy", + PanicGuide: h.panicGuide, + Checker: h.producer.ConnectivityCheck, } - return errMsg, errors.New(errMsg) } -func (service *healthService) checkMessageQueueProxyReachable(address string, topic string, authKey string, queue string) error { - req, err := http.NewRequest("GET", address+"/topics", nil) - if err != nil { - logger.messageEvent(topic, fmt.Sprintf("Could not connect to proxy: %v", err.Error())) - return err +func (h *HealthCheck) GTG() gtg.Status { + consumerCheck := func() gtg.Status { + return gtgCheck(h.consumer.ConnectivityCheck) } - if len(authKey) > 0 { - req.Header.Add("Authorization", authKey) + producerCheck := func() gtg.Status { + return gtgCheck(h.producer.ConnectivityCheck) } - if len(queue) > 0 { - req.Host = queue - } - resp, err := service.config.httpCl.Do(req) - if err != nil { - logger.messageEvent(topic, fmt.Sprintf("Could not connect to proxy: %v", err.Error())) - return err - } - defer func() { _ = resp.Body.Close() }() - if resp.StatusCode != http.StatusOK { - errMsg := fmt.Sprintf("Proxy returned status: %d", resp.StatusCode) - return errors.New(errMsg) - } - return nil + + return gtg.FailFastParallelCheck([]gtg.StatusChecker{ + consumerCheck, + producerCheck, + })() } -func (service *healthService) gtgCheck() gtg.Status { - for _, check := range service.checks { - if _, err := check.Checker(); err != nil { - return gtg.Status{GoodToGo: false, Message: err.Error()} - } +func gtgCheck(handler func() (string, error)) gtg.Status { + if _, err := handler(); err != nil { + return gtg.Status{GoodToGo: false, Message: err.Error()} } return gtg.Status{GoodToGo: true} } diff --git a/healthchecks_test.go b/healthchecks_test.go index b6d1809..5d60e6e 100644 --- a/healthchecks_test.go +++ b/healthchecks_test.go @@ -1,134 +1,140 @@ package main import ( + "errors" + "net/http/httptest" + "testing" + "github.com/Financial-Times/message-queue-go-producer/producer" "github.com/Financial-Times/message-queue-gonsumer/consumer" - "github.com/gorilla/handlers" - "github.com/gorilla/mux" "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - "testing" ) -const ( - statusOK int = 1 + iota - statusNA -) +func initializeHealthCheck(isProducerConnectionHealthy bool, isConsumerConnectionHealthy bool) *HealthCheck { + return &HealthCheck{ + consumer: &mockConsumerInstance{isConnectionHealthy: isConsumerConnectionHealthy}, + producer: &mockProducerInstance{isConnectionHealthy: isProducerConnectionHealthy}, + } +} -var queueServerMock *httptest.Server +func TestNewHealthCheck(t *testing.T) { + c := &consumer.QueueConfig{} + p := &producer.MessageProducerConfig{} + hc := newHealthCheck(p, c, "appName", "systemCode", "panicGuide") -func init() { - logger = newAppLogger("test") + assert.NotNil(t, hc.consumer) + assert.NotNil(t, hc.producer) + assert.Equal(t, "appName", hc.appName) + assert.Equal(t, "systemCode", hc.appSystemCode) + assert.Equal(t, "panicGuide", hc.panicGuide) } -func TestCheckMessageQueueAvailability(t *testing.T) { - assert := assert.New(t) - - startQueueServerMock(statusOK) - defer queueServerMock.Close() +func TestHappyHealthCheck(t *testing.T) { + hc := initializeHealthCheck(true, true) - hc := healthConfig{ - httpCl: &http.Client{}, - consumerConf: newConsumerConfig(queueServerMock.URL), - producerConf: newProducerConfig(queueServerMock.URL), - } + req := httptest.NewRequest("GET", "http://example.com/__health", nil) + w := httptest.NewRecorder() - hs := newHealthService(&hc) + hc.Health()(w, req) - result, err := hs.checkAggregateMessageQueueProxiesReachable() - assert.Nil(err, "Error not expected.") - assert.Equal("Ok", result, "Message queue availability status is wrong") + assert.Equal(t, 200, w.Code, "It should return HTTP 200 OK") + assert.Contains(t, w.Body.String(), `"name":"Read Message Queue Proxy Reachable","ok":true`, "Read message queue proxy healthcheck should be happy") + assert.Contains(t, w.Body.String(), `"name":"Write Message Queue Proxy Reachable","ok":true`, "Write message queue proxy healthcheck should be happy") } -func TestCheckMessageQueueNonAvailability(t *testing.T) { - assert := assert.New(t) +func TestHealthCheckWithUnhappyConsumer(t *testing.T) { + hc := initializeHealthCheck(true, false) - startQueueServerMock(statusNA) - defer queueServerMock.Close() + req := httptest.NewRequest("GET", "http://example.com/__health", nil) + w := httptest.NewRecorder() - hc := healthConfig{ - httpCl: &http.Client{}, - consumerConf: newConsumerConfig(queueServerMock.URL), - producerConf: newProducerConfig(queueServerMock.URL), - } + hc.Health()(w, req) + + assert.Equal(t, 200, w.Code, "It should return HTTP 200 OK") + assert.Contains(t, w.Body.String(), `"name":"Read Message Queue Proxy Reachable","ok":false`, "Read message queue proxy healthcheck should be unhappy") + assert.Contains(t, w.Body.String(), `"name":"Write Message Queue Proxy Reachable","ok":true`, "Write message queue proxy healthcheck should be happy") +} + +func TestHealthCheckWithUnhappyProducer(t *testing.T) { + hc := initializeHealthCheck(false, true) - hs := newHealthService(&hc) + req := httptest.NewRequest("GET", "http://example.com/__health", nil) + w := httptest.NewRecorder() - _, err := hs.checkAggregateMessageQueueProxiesReachable() - assert.Equal(true, err != nil, "Error was expected.") + hc.Health()(w, req) + + assert.Equal(t, 200, w.Code, "It should return HTTP 200 OK") + assert.Contains(t, w.Body.String(), `"name":"Read Message Queue Proxy Reachable","ok":true`, "Read message queue proxy healthcheck should be happy") + assert.Contains(t, w.Body.String(), `"name":"Write Message Queue Proxy Reachable","ok":false`, "Write message queue proxy healthcheck should be unhappy") } -func TestCheckMessageQueueWrongQueueURL(t *testing.T) { - assert := assert.New(t) +func TestUnhappyHealthCheck(t *testing.T) { + hc := initializeHealthCheck(false, false) - startQueueServerMock(statusOK) - defer queueServerMock.Close() + req := httptest.NewRequest("GET", "http://example.com/__health", nil) + w := httptest.NewRecorder() - tests := []struct { - consumerConfig consumer.QueueConfig - producerConfig producer.MessageProducerConfig - }{ - { - newConsumerConfig("wrong url"), - newProducerConfig(queueServerMock.URL), - }, - { - newConsumerConfig(queueServerMock.URL), - newProducerConfig("wrong url"), - }, - } + hc.Health()(w, req) - for _, test := range tests { - hc := healthConfig{ - httpCl: &http.Client{}, - consumerConf: test.consumerConfig, - producerConf: test.producerConfig, - } + assert.Equal(t, 200, w.Code, "It should return HTTP 200 OK") + assert.Contains(t, w.Body.String(), `"name":"Read Message Queue Proxy Reachable","ok":false`, "Read message queue proxy healthcheck should be unhappy") + assert.Contains(t, w.Body.String(), `"name":"Write Message Queue Proxy Reachable","ok":false`, "Write message queue proxy healthcheck should be unhappy") +} - hs := newHealthService(&hc) +func TestGTGHappyFlow(t *testing.T) { + hc := initializeHealthCheck(true, true) - _, err := hs.checkAggregateMessageQueueProxiesReachable() - assert.Equal(true, err != nil, "Error was expected for input consumer [%v], producer [%v]", test.consumerConfig, test.producerConfig) - } + status := hc.GTG() + assert.True(t, status.GoodToGo) + assert.Empty(t, status.Message) } -func startQueueServerMock(status int) { - router := mux.NewRouter() - var getContent http.HandlerFunc +func TestGTGBrokenConsumer(t *testing.T) { + hc := initializeHealthCheck(true, false) - switch status { - case statusOK: - getContent = statusOKHandler - case statusNA: - getContent = internalErrorHandler - } + status := hc.GTG() + assert.False(t, status.GoodToGo) + assert.Equal(t, "Error connecting to the queue", status.Message) +} + +func TestGTGBrokenProducer(t *testing.T) { + hc := initializeHealthCheck(false, true) - router.Path("/topics").Handler(handlers.MethodHandler{"GET": http.HandlerFunc(getContent)}) + status := hc.GTG() + assert.False(t, status.GoodToGo) + assert.Equal(t, "Error connecting to the queue", status.Message) +} - queueServerMock = httptest.NewServer(router) +type mockProducerInstance struct { + isConnectionHealthy bool } -func statusOKHandler(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) +type mockConsumerInstance struct { + isConnectionHealthy bool } -func internalErrorHandler(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) +func (p *mockProducerInstance) SendMessage(string, producer.Message) error { + return nil } -func newConsumerConfig(addr string) consumer.QueueConfig { - return consumer.QueueConfig{ - Addrs: []string{addr}, - Queue: "queue", - AuthorizationKey: "auth", +func (p *mockProducerInstance) ConnectivityCheck() (string, error) { + if p.isConnectionHealthy { + return "", nil } + + return "", errors.New("Error connecting to the queue") +} + +func (c *mockConsumerInstance) Start() { } -func newProducerConfig(addr string) producer.MessageProducerConfig { - return producer.MessageProducerConfig{ - Addr: addr, - Queue: "queue", - Authorization: "auth", +func (c *mockConsumerInstance) Stop() { +} + +func (c *mockConsumerInstance) ConnectivityCheck() (string, error) { + if c.isConnectionHealthy { + return "", nil } + + return "", errors.New("Error connecting to the queue") } diff --git a/main.go b/main.go index 7c71e1b..f781081 100644 --- a/main.go +++ b/main.go @@ -1,19 +1,19 @@ package main import ( - health "github.com/Financial-Times/go-fthealth/v1_1" - "github.com/Financial-Times/message-queue-go-producer/producer" - "github.com/Financial-Times/message-queue-gonsumer/consumer" - status "github.com/Financial-Times/service-status-go/httphandlers" - log "github.com/Sirupsen/logrus" - "github.com/gorilla/handlers" - "github.com/jawher/mow.cli" "net/http" "os" "os/signal" "sync" "syscall" "time" + + "github.com/Financial-Times/message-queue-go-producer/producer" + "github.com/Financial-Times/message-queue-gonsumer/consumer" + status "github.com/Financial-Times/service-status-go/httphandlers" + log "github.com/Sirupsen/logrus" + "github.com/gorilla/handlers" + "github.com/jawher/mow.cli" ) const serviceDescription = "Get the related content references from the Next video content, creates a story package holding those references and puts a message with them on kafka queue for further processing and ingestion on Neo4j." @@ -55,6 +55,12 @@ func main() { Desc: "Port to listen on", EnvVar: "APP_PORT", }) + panicGuide := app.String(cli.StringOpt{ + Name: "panic-guide", + Value: "https://dewey.ft.com/upp-next-video-cc-mapper.html", + Desc: "Path to panic guide", + EnvVar: "PANIC_GUIDE", + }) addresses := app.Strings(cli.StringsOpt{ Name: "queue-addresses", Value: []string{"http://localhost:8080"}, @@ -122,17 +128,10 @@ func main() { Queue: *writeQueue, } - hc := healthConfig{ - appSystemCode: *appSystemCode, - appName: *appName, - port: *port, - httpCl: httpCl, - consumerConf: consumerConfig, - producerConf: producerConfig, - } + hc := newHealthCheck(&producerConfig, &consumerConfig, *appName, *appSystemCode, *panicGuide) go func() { - serveAdminEndpoints(*appSystemCode, sc, sh, hc) + serveAdminEndpoints(sc, sh, hc) }() qh := queueHandler{sc: sc, httpCl: httpCl, consumerConfig: consumerConfig, producerConfig: producerConfig} @@ -147,16 +146,12 @@ func main() { } } -func serveAdminEndpoints(appSystemCode string, sc serviceConfig, sh serviceHandler, hc healthConfig) { - +func serveAdminEndpoints(sc serviceConfig, sh serviceHandler, hc *HealthCheck) { serveMux := http.NewServeMux() serveMux.Handle("/map", handlers.MethodHandler{"POST": http.HandlerFunc(sh.mapRequest)}) - - healthService := newHealthService(&hc) - h := health.HealthCheck{SystemCode: appSystemCode, Name: sc.appName, Description: serviceDescription, Checks: healthService.checks} - serveMux.HandleFunc(healthPath, health.Handler(h)) - serveMux.HandleFunc(status.GTGPath, status.NewGoodToGoHandler(healthService.gtgCheck)) + serveMux.HandleFunc("/__health", hc.Health()) + serveMux.HandleFunc(status.GTGPath, status.NewGoodToGoHandler(hc.GTG)) serveMux.HandleFunc(status.BuildInfoPath, status.BuildInfoHandler) logger.serviceStartedEvent(sc.asMap()) diff --git a/mapper.go b/mapper.go index 82776c5..f30f93e 100644 --- a/mapper.go +++ b/mapper.go @@ -2,9 +2,10 @@ package main import ( "encoding/json" + "errors" "fmt" + uuidUtils "github.com/Financial-Times/uuid-utils-go" - "errors" ) const ( diff --git a/vendor/vendor.json b/vendor/vendor.json index f5f9ce5..7f32640 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -3,7 +3,7 @@ "ignore": "test", "package": [ { - "checksumSHA1": "TVyK4buHhPUkpD0Pt9M7Zo2UAlI=", + "checksumSHA1": "2dtgENfeO6SICvxjruwb9f1CzLY=", "path": "github.com/Financial-Times/go-fthealth/v1_1", "revision": "bc27ed19189994eef0364e4a07879d5a3f3be76f", "revisionTime": "2017-03-24T12:21:32Z", @@ -11,23 +11,23 @@ "versionExact": "0.2.0" }, { - "checksumSHA1": "bagfBc+hPfspolZec6sixTKb3Ec=", + "checksumSHA1": "GrgODPqXv7QebQjfjrxMHctK0wE=", "path": "github.com/Financial-Times/message-queue-go-producer/producer", - "revision": "163efbf93f1a2ba839b317d26e37426bcfa3850a", - "revisionTime": "2017-03-20T10:55:12Z", - "version": "v0.1.0", - "versionExact": "v0.1.0" + "revision": "e0d20e2c97fff205138d33c3dee9347878640b5e", + "revisionTime": "2017-05-26T06:27:13Z", + "version": "0.2.0-xp-remove-kafka-topic-check-rc.1", + "versionExact": "0.2.0-xp-remove-kafka-topic-check-rc.1" }, { - "checksumSHA1": "6R+ptya8zWzpWfv27a9PdmH6uVo=", + "checksumSHA1": "mirNgwWRdRte0ku/g4u4fNbzQP0=", "path": "github.com/Financial-Times/message-queue-gonsumer/consumer", - "revision": "4e173298922ba4c10721b1b5b3e0d35be7f162e9", - "revisionTime": "2017-03-20T10:34:32Z", - "version": "0.3.0", - "versionExact": "0.3.0" + "revision": "0a481ff94df47f766a74867475247eaf1ed40107", + "revisionTime": "2017-05-26T12:08:30Z", + "version": "0.4.0-xp-remove-kafka-topic-check-rc.1", + "versionExact": "0.4.0-xp-remove-kafka-topic-check-rc.1" }, { - "checksumSHA1": "lQfsRf7gWYQDNoI3IOZuwNGUwRo=", + "checksumSHA1": "fqpohN7Qp2qj7TLMbUxh/cK4oH0=", "path": "github.com/Financial-Times/service-status-go/buildinfo", "revision": "3f5199736a3d7ae52394c63aac36834786825e21", "revisionTime": "2016-03-23T11:15:42Z", @@ -35,7 +35,7 @@ "versionExact": "0.1.0" }, { - "checksumSHA1": "7QAsTdXi/6nTkDhqKy54YIc69d4=", + "checksumSHA1": "2rGNLXdRC3qr5n5ll7BpYt8HCK8=", "path": "github.com/Financial-Times/service-status-go/gtg", "revision": "3f5199736a3d7ae52394c63aac36834786825e21", "revisionTime": "2016-03-23T11:15:42Z", @@ -43,7 +43,7 @@ "versionExact": "0.1.0" }, { - "checksumSHA1": "YxgjuCI4TJyfQujUeQk3V+gypzo=", + "checksumSHA1": "973POGyMCoyaKQuIYX2f7EKFwlQ=", "path": "github.com/Financial-Times/service-status-go/httphandlers", "revision": "3f5199736a3d7ae52394c63aac36834786825e21", "revisionTime": "2016-03-23T11:15:42Z", @@ -51,7 +51,7 @@ "versionExact": "0.1.0" }, { - "checksumSHA1": "CphoWOjfAWc2hFo1e9SBuLaAnMU=", + "checksumSHA1": "jQY18Qfl75Zam/qp4Tu45Y82NyI=", "path": "github.com/Financial-Times/uuid-utils-go", "revision": "6a93dec4ed9646eb79596fcac3fda379974d9d2f", "revisionTime": "2017-05-12T09:08:27Z", @@ -59,63 +59,63 @@ "versionExact": "1.0.0" }, { - "checksumSHA1": "NSJhlFlJwKJ90vd+jpwiXY/VYh0=", + "checksumSHA1": "ZKxETlJdB2XubMrZnXB0FQimVA8=", "path": "github.com/Sirupsen/logrus", "revision": "10f801ebc38b33738c9d17d50860f484a0988ff5", "revisionTime": "2017-03-17T14:32:14Z" }, { - "checksumSHA1": "DuEyF75v9xaKXfJsCPRdHNOpGZk=", + "checksumSHA1": "OFu4xJEIjiI8Suu+j/gabfp+y6Q=", "origin": "github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew", "path": "github.com/davecgh/go-spew/spew", "revision": "4d4bfba8f1d1027c4fdbe371823030df51419987", "revisionTime": "2017-01-30T11:31:45Z" }, { - "checksumSHA1": "xUSFQA1C5QOnWP1bA/1AvzfeycA=", + "checksumSHA1": "+/oy2SsS8YvHVvQGcWUDbaajeXQ=", "path": "github.com/gorilla/handlers", "revision": "13d73096a474cac93275c679c7b8a2dc17ddba82", "revisionTime": "2017-02-24T19:39:55Z" }, { - "checksumSHA1": "CmsKJLZZkfiiiOI9MKkFv3XL+Mo=", + "checksumSHA1": "zmCk+lgIeiOf0Ng9aFP9aFy8ksE=", "path": "github.com/gorilla/mux", "revision": "4c1c3952b7d9d0a061a3fa7b36fd373ba0398ebc", "revisionTime": "2017-04-27T04:12:50Z" }, { - "checksumSHA1": "KJqRW8jfPoHquMAd6FI7x92JxFs=", + "checksumSHA1": "tUGxc7rfX0cmhOOUDhMuAZ9rWsA=", "path": "github.com/hashicorp/go-version", "revision": "03c5bf6be031b6dd45afec16b1cf94fc8938bc77", "revisionTime": "2017-02-02T08:07:59Z" }, { - "checksumSHA1": "CRYmanmmriS4QPTxS4mM9KhcZI0=", + "checksumSHA1": "NK3DaezXmT5QyDImKH+qqI2Xa7c=", "path": "github.com/jawher/mow.cli", "revision": "8327d12beb75e6471b7f045588acc318d1147146", "revisionTime": "2017-04-30T13:52:12Z" }, { - "checksumSHA1": "+oyIJwPyeof36XCkY8awrNfxaNM=", + "checksumSHA1": "zKKp5SZ3d3ycKe4EKMNT0BqAWBw=", "origin": "github.com/stretchr/testify/vendor/github.com/pmezard/go-difflib/difflib", "path": "github.com/pmezard/go-difflib/difflib", "revision": "4d4bfba8f1d1027c4fdbe371823030df51419987", "revisionTime": "2017-01-30T11:31:45Z" }, { - "checksumSHA1": "HUXE+Nrcau8FSaVEvPYHMvDjxOE=", + "checksumSHA1": "zmC8/3V4ls53DJlNTKDZwPSC/dA=", "path": "github.com/satori/go.uuid", "revision": "5bf94b69c6b68ee1b541973bb8e1144db23a194b", "revisionTime": "2017-03-21T23:07:31Z" }, { - "checksumSHA1": "ziYrpyU5zo1q9+n1kaXqkR3kP2s=", + "checksumSHA1": "JXUVA1jky8ZX8w09p2t5KLs97Nc=", "path": "github.com/stretchr/testify/assert", "revision": "4d4bfba8f1d1027c4fdbe371823030df51419987", "revisionTime": "2017-01-30T11:31:45Z" }, { - "checksumSHA1": "hRpSBofTFAM50mVLe4sID2rP2v8=", + "checksumSHA1": "6KZEap6v/5gD+tjiNbpD81kD61o=", "path": "github.com/willf/bitset", "revision": "988f4f24992fc745de53c42df0da6581e42a6686", "revisionTime": "2017-05-05T19:16:46Z"