diff --git a/.gitignore b/.gitignore index 9809201..99dab0b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,5 @@ /notifications-push-client /.env *.iml -vendor/*/ + /.project diff --git a/Dockerfile b/Dockerfile index df23288..6f0e539 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,31 +1,27 @@ -FROM golang:1.8-alpine +FROM alpine:3.5 -ENV PROJECT=notifications-push -COPY . /${PROJECT}-sources/ +COPY . .git /notifications-push/ -RUN apk --no-cache --virtual .build-dependencies add git \ - && ORG_PATH="github.com/Financial-Times" \ - && REPO_PATH="${ORG_PATH}/${PROJECT}" \ - && mkdir -p $GOPATH/src/${ORG_PATH} \ - # Linking the project sources in the GOPATH folder - && ln -s /${PROJECT}-sources $GOPATH/src/${REPO_PATH} \ +RUN apk --update add git go libc-dev \ + && export GOPATH=/gopath \ + && REPO_PATH="github.com/Financial-Times/notifications-push" \ + && mkdir -p $GOPATH/src/${REPO_PATH} \ + && mv /notifications-push/* $GOPATH/src/${REPO_PATH} \ && cd $GOPATH/src/${REPO_PATH} \ - && BUILDINFO_PACKAGE="${ORG_PATH}/${PROJECT}/vendor/${ORG_PATH}/service-status-go/buildinfo." \ + && BUILDINFO_PACKAGE="github.com/Financial-Times/service-status-go/buildinfo." \ && VERSION="version=$(git describe --tag --always 2> /dev/null)" \ && DATETIME="dateTime=$(date -u +%Y%m%d%H%M%S)" \ && REPOSITORY="repository=$(git config --get remote.origin.url)" \ && REVISION="revision=$(git rev-parse HEAD)" \ && BUILDER="builder=$(go version)" \ && LDFLAGS="-X '"${BUILDINFO_PACKAGE}$VERSION"' -X '"${BUILDINFO_PACKAGE}$DATETIME"' -X '"${BUILDINFO_PACKAGE}$REPOSITORY"' -X '"${BUILDINFO_PACKAGE}$REVISION"' -X '"${BUILDINFO_PACKAGE}$BUILDER"'" \ - && echo "Build flags: $LDFLAGS" \ - && echo "Fetching dependencies..." \ - && go get -u github.com/kardianos/govendor \ - && $GOPATH/bin/govendor sync \ + && echo $LDFLAGS \ + && go get -t ./... \ && go build -ldflags="${LDFLAGS}" \ - && mv ${PROJECT} /${PROJECT}-app \ - && apk del .build-dependencies \ + && mv notifications-push /notifications-push-app \ + && rm -rf /notifications-push \ + && mv /notifications-push-app /notifications-push \ + && apk del go git libc-dev \ && rm -rf $GOPATH /var/cache/apk/* -WORKDIR / - -CMD [ "/notifications-push-app" ] \ No newline at end of file +CMD [ "/notifications-push" ] diff --git a/app.go b/app.go index 6f70c98..1e9afb9 100644 --- a/app.go +++ b/app.go @@ -1,8 +1,6 @@ package main import ( - log "github.com/Sirupsen/logrus" - "github.com/gorilla/mux" "net/http" _ "net/http/pprof" "os" @@ -11,7 +9,9 @@ import ( "strings" "time" - "fmt" + log "github.com/Sirupsen/logrus" + "github.com/gorilla/mux" + fthealth "github.com/Financial-Times/go-fthealth/v1a" queueConsumer "github.com/Financial-Times/message-queue-gonsumer/consumer" "github.com/Financial-Times/notifications-push/consumer" @@ -19,13 +19,13 @@ import ( "github.com/Financial-Times/notifications-push/resources" "github.com/Financial-Times/service-status-go/httphandlers" "github.com/jawher/mow.cli" - "net" ) const heartbeatPeriod = 30 * time.Second func init() { - f := &log.JSONFormatter{ + f := &log.TextFormatter{ + FullTimestamp: true, TimestampFormat: time.RFC3339Nano, } @@ -76,12 +76,6 @@ func main() { Desc: "The API base URL where resources are accessible", EnvVar: "API_BASE_URL", }) - apiKeyValidationEndpoint := app.String(cli.StringOpt{ - Name: "api_key_validation_endpoint", - Value: "t800/a", - Desc: "The Mashery ApiKey validation endpoint", - EnvVar: "API_KEY_VALIDATION_ENDPOINT", - }) topic := app.String(cli.StringOpt{ Name: "topic", Value: "", @@ -144,22 +138,9 @@ func main() { } queueHandler := consumer.NewMessageQueueHandler(whitelistR, mapper, dispatcher) + consumer := queueConsumer.NewBatchedConsumer(consumerConfig, queueHandler.HandleMessage, &http.Client{}) - tr := &http.Transport{ - MaxIdleConnsPerHost: 32, - Dial: (&net.Dialer{ - Timeout: 10 * time.Second, - KeepAlive: 30 * time.Second, - }).Dial, - } - httpClient := &http.Client{ - Transport: tr, - Timeout: time.Duration(10 * time.Second), - } - - consumer := queueConsumer.NewBatchedConsumer(consumerConfig, queueHandler.HandleMessage, httpClient) - masheryApiKeyValidationUrl := fmt.Sprintf("%s/%s", *apiBaseURL, *apiKeyValidationEndpoint) - go server(":"+strconv.Itoa(*port), *resource, dispatcher, history, consumerConfig, masheryApiKeyValidationUrl, httpClient) + go server(":"+strconv.Itoa(*port), *resource, dispatcher, history, consumerConfig) pushService := newPushService(dispatcher, consumer) pushService.start() @@ -170,12 +151,12 @@ func main() { } } -func server(listen string, resource string, dispatcher dispatcher.Dispatcher, history dispatcher.History, consumerConfig queueConsumer.QueueConfig, masheryApiKeyValidationUrl string, httpClient *http.Client) { +func server(listen string, resource string, dispatcher dispatcher.Dispatcher, history dispatcher.History, consumerConfig queueConsumer.QueueConfig) { notificationsPushPath := "/" + resource + "/notifications-push" r := mux.NewRouter() - r.HandleFunc(notificationsPushPath, resources.Push(dispatcher, masheryApiKeyValidationUrl, httpClient)).Methods("GET") + r.HandleFunc(notificationsPushPath, resources.Push(dispatcher)).Methods("GET") r.HandleFunc("/__history", resources.History(history)).Methods("GET") r.HandleFunc("/__stats", resources.Stats(dispatcher)).Methods("GET") diff --git a/circle.yml b/circle.yml index 72d5d76..601a021 100644 --- a/circle.yml +++ b/circle.yml @@ -1,30 +1,32 @@ machine: environment: - PROJECT_GOPATH: "${HOME}/.go_project" - PROJECT_PARENT_PATH: "${PROJECT_GOPATH}/src/github.com/${CIRCLE_PROJECT_USERNAME}" - PROJECT_PATH: "${PROJECT_PARENT_PATH}/${CIRCLE_PROJECT_REPONAME}" - GOPATH: "${HOME}/.go_workspace:${PROJECT_GOPATH}" - -checkout: + GODIST: "go1.7.3.linux-amd64.tar.gz" post: - - mkdir -p "${PROJECT_PARENT_PATH}" - - ln -sf "${HOME}/${CIRCLE_PROJECT_REPONAME}/" "${PROJECT_PATH}" - + - mkdir -p download + - test -e download/$GODIST || curl -o download/$GODIST https://storage.googleapis.com/golang/$GODIST + - sudo rm -rf /usr/local/go + - sudo tar -C /usr/local -xzf download/$GODIST + services: + - docker dependencies: pre: - - go get -u github.com/kardianos/govendor - override: - - cd $PROJECT_PATH && govendor sync - - cd $PROJECT_PATH && go build -v - + - go get github.com/axw/gocov/gocov; go get github.com/matm/gocov-html; go get -u github.com/jstemmer/go-junit-report test: pre: - - go get -u github.com/jstemmer/go-junit-report - - go get -u github.com/mattn/goveralls - - cd $PROJECT_PATH && wget https://raw.githubusercontent.com/Financial-Times/cookiecutter-upp-golang/master/coverage.sh && chmod +x coverage.sh + - go get github.com/mattn/goveralls override: - mkdir -p $CIRCLE_TEST_REPORTS/golang - - cd $PROJECT_PATH && govendor test -race -v +local | go-junit-report > $CIRCLE_TEST_REPORTS/golang/junit.xml - - cd $PROJECT_PATH && ./coverage.sh + - go test -race -v ./... | go-junit-report > $CIRCLE_TEST_REPORTS/golang/junit.xml + - go test -v -cover -race -coverprofile=$CIRCLE_ARTIFACTS/consumer.out ./consumer + - go test -v -cover -race -coverprofile=$CIRCLE_ARTIFACTS/dispatcher.out ./dispatcher + - go test -v -cover -race -coverprofile=$CIRCLE_ARTIFACTS/resources.out ./resources + - cd $CIRCLE_ARTIFACTS && sed -i '1d' *.out + - | + echo "mode: atomic" > $CIRCLE_ARTIFACTS/overall-coverage.result + - cd $CIRCLE_ARTIFACTS && cat *.out >> overall-coverage.result + - docker build --rm=false -t test/notifications-push . + - docker run -d -p 9200:8080 test/notifications-push; sleep 3 + - curl --retry 10 --retry-delay 5 -v http://localhost:9200/__health post: - - goveralls -coverprofile=$CIRCLE_ARTIFACTS/coverage.out -service=circle-ci -repotoken=$COVERALLS_TOKEN + - goveralls -coverprofile=$CIRCLE_ARTIFACTS/overall-coverage.result -service=circle-ci -repotoken=$COVERALLS_TOKEN + - bash <(curl -s https://codecov.io/bash) \ No newline at end of file diff --git a/resources/push.go b/resources/push.go index 4ec574a..a162a38 100644 --- a/resources/push.go +++ b/resources/push.go @@ -10,23 +10,8 @@ import ( log "github.com/Sirupsen/logrus" ) -const ( - apiKeyHeaderField = "X-Api-Key" - apiKeyQueryParam = "apiKey" -) - -//ApiKey is provided either as a request param or as a header. -func getApiKey(r *http.Request) string { - apiKey := r.Header.Get(apiKeyHeaderField) - if apiKey != "" { - return apiKey - } - - return r.URL.Query().Get(apiKeyQueryParam) -} - // Push handler for push subscribers -func Push(reg dispatcher.Registrar, masheryApiKeyValidationURL string, httpClient *http.Client) func(w http.ResponseWriter, r *http.Request) { +func Push(reg dispatcher.Registrar) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") @@ -34,12 +19,6 @@ func Push(reg dispatcher.Registrar, masheryApiKeyValidationURL string, httpClien w.Header().Set("Pragma", "no-cache") w.Header().Set("Expires", "0") - apiKey := getApiKey(r) - if isValid, errMsg, errStatusCode := isValidApiKey(apiKey, masheryApiKeyValidationURL, httpClient); !isValid { - http.Error(w, errMsg, errStatusCode) - return - } - cn, ok := w.(http.CloseNotifier) if !ok { http.Error(w, "Cannot stream.", http.StatusInternalServerError) diff --git a/resources/push_test.go b/resources/push_test.go index 12fb14c..0cde59f 100644 --- a/resources/push_test.go +++ b/resources/push_test.go @@ -7,13 +7,10 @@ import ( "testing" "time" - "errors" "github.com/Financial-Times/notifications-push/dispatcher" "github.com/Financial-Times/notifications-push/test/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" - "io/ioutil" - "strings" ) var start func(sub dispatcher.Subscriber) @@ -31,7 +28,6 @@ func TestPushStandardSubscriber(t *testing.T) { } req.Header.Set("X-Forwarded-For", "some-host, some-other-host-that-isnt-used") - req.Header.Set(apiKeyHeaderField, "some-api-key") start = func(sub dispatcher.Subscriber) { sub.NotificationChannel() <- "hi" @@ -42,10 +38,7 @@ func TestPushStandardSubscriber(t *testing.T) { assert.Equal(t, "some-host", sub.Address()) } - httpClient := initializeMockHTTPClient(&mockTransport{ - responseStatusCode: http.StatusOK, - }) - Push(d, "http://dummy.ft.com", httpClient)(w, req) + Push(d)(w, req) assert.Equal(t, "text/event-stream", w.Header().Get("Content-Type"), "Should be SSE") assert.Equal(t, "no-cache, no-store, must-revalidate", w.Header().Get("Cache-Control")) @@ -58,7 +51,7 @@ func TestPushStandardSubscriber(t *testing.T) { assert.Equal(t, "data: hi\n\n", body) - assert.Equal(t, http.StatusOK, w.Code, "Should be OK") + assert.Equal(t, 200, w.Code, "Should be OK") d.AssertExpectations(t) } @@ -75,7 +68,6 @@ func TestPushMonitorSubscriber(t *testing.T) { } req.Header.Set("X-Forwarded-For", "some-host, some-other-host-that-isnt-used") - req.Header.Set(apiKeyHeaderField, "some-api-key") start = func(sub dispatcher.Subscriber) { sub.NotificationChannel() <- "hi" @@ -86,10 +78,7 @@ func TestPushMonitorSubscriber(t *testing.T) { assert.Equal(t, "some-host", sub.Address()) } - httpClient := initializeMockHTTPClient(&mockTransport{ - responseStatusCode: http.StatusOK, - }) - Push(d, "http://dummy.ft.com", httpClient)(w, req) + Push(d)(w, req) assert.Equal(t, "text/event-stream", w.Header().Get("Content-Type"), "Should be SSE") assert.Equal(t, "no-cache, no-store, must-revalidate", w.Header().Get("Cache-Control")) @@ -102,7 +91,7 @@ func TestPushMonitorSubscriber(t *testing.T) { assert.Equal(t, "data: hi\n\n", body) - assert.Equal(t, http.StatusOK, w.Code, "Should be OK") + assert.Equal(t, 200, w.Code, "Should be OK") d.AssertExpectations(t) } @@ -113,165 +102,9 @@ func TestPushFailed(t *testing.T) { if err != nil { t.Fatal(err) } - req.Header.Set(apiKeyHeaderField, "some-api-key") - httpClient := initializeMockHTTPClient(&mockTransport{ - responseStatusCode: http.StatusOK, - }) - Push(d, "http://dummy.ft.com", httpClient)(w, req) - assert.Equal(t, http.StatusInternalServerError, w.Code) -} - -func TestMasheryDown(t *testing.T) { - d := new(MockDispatcher) - - d.On("Register", mock.AnythingOfType("*dispatcher.standardSubscriber")).Return() - d.On("Close", mock.AnythingOfType("*dispatcher.standardSubscriber")).Return() - - w := NewStreamResponseRecorder() - req, err := http.NewRequest("GET", "/content/notifications-push", nil) - if err != nil { - t.Fatal(err) - } - - req.Header.Set("X-Forwarded-For", "some-host, some-other-host-that-isnt-used") - req.Header.Set(apiKeyHeaderField, "some-api-key") - - start = func(sub dispatcher.Subscriber) { - sub.NotificationChannel() <- "hi" - time.Sleep(10 * time.Millisecond) - w.closer <- true - - assert.True(t, time.Now().After(sub.Since())) - assert.Equal(t, "some-host", sub.Address()) - } - - httpClient := initializeMockHTTPClient(&mockTransport{ - responseStatusCode: http.StatusInternalServerError, - }) - Push(d, "http://dummy.ft.com", httpClient)(w, req) - assert.Equal(t, http.StatusServiceUnavailable, w.Code) -} - -func TestInvalidApiKey(t *testing.T) { - d := new(MockDispatcher) - - d.On("Register", mock.AnythingOfType("*dispatcher.standardSubscriber")).Return() - d.On("Close", mock.AnythingOfType("*dispatcher.standardSubscriber")).Return() - - w := NewStreamResponseRecorder() - req, err := http.NewRequest("GET", "/content/notifications-push", nil) - if err != nil { - t.Fatal(err) - } - - req.Header.Set("X-Forwarded-For", "some-host, some-other-host-that-isnt-used") - req.Header.Set(apiKeyHeaderField, "some-wrong-api-key") - - start = func(sub dispatcher.Subscriber) { - sub.NotificationChannel() <- "hi" - time.Sleep(10 * time.Millisecond) - w.closer <- true - - assert.True(t, time.Now().After(sub.Since())) - assert.Equal(t, "some-host", sub.Address()) - } - - httpClient := initializeMockHTTPClient(&mockTransport{ - responseStatusCode: http.StatusUnauthorized, - }) - Push(d, "http://dummy.ft.com", httpClient)(w, req) - assert.Equal(t, http.StatusUnauthorized, w.Code) -} - -func TestEmptyApiKey(t *testing.T) { - d := new(MockDispatcher) - - d.On("Register", mock.AnythingOfType("*dispatcher.standardSubscriber")).Return() - d.On("Close", mock.AnythingOfType("*dispatcher.standardSubscriber")).Return() - - w := NewStreamResponseRecorder() - req, err := http.NewRequest("GET", "/content/notifications-push", nil) - if err != nil { - t.Fatal(err) - } - - req.Header.Set("X-Forwarded-For", "some-host, some-other-host-that-isnt-used") - req.Header.Set(apiKeyHeaderField, "") - - start = func(sub dispatcher.Subscriber) { - sub.NotificationChannel() <- "hi" - time.Sleep(10 * time.Millisecond) - w.closer <- true - - assert.True(t, time.Now().After(sub.Since())) - assert.Equal(t, "some-host", sub.Address()) - } - - httpClient := initializeMockHTTPClient(&mockTransport{}) - Push(d, "http://dummy.ft.com", httpClient)(w, req) - assert.Equal(t, http.StatusUnauthorized, w.Code) -} - -func TestInvalidUrlForValidatingApiKey(t *testing.T) { - d := new(MockDispatcher) - - d.On("Register", mock.AnythingOfType("*dispatcher.standardSubscriber")).Return() - d.On("Close", mock.AnythingOfType("*dispatcher.standardSubscriber")).Return() - - w := NewStreamResponseRecorder() - req, err := http.NewRequest("GET", "/content/notifications-push", nil) - if err != nil { - t.Fatal(err) - } - - req.Header.Set("X-Forwarded-For", "some-host, some-other-host-that-isnt-used") - req.Header.Set(apiKeyHeaderField, "some-api-key") - - start = func(sub dispatcher.Subscriber) { - sub.NotificationChannel() <- "hi" - time.Sleep(10 * time.Millisecond) - w.closer <- true - - assert.True(t, time.Now().After(sub.Since())) - assert.Equal(t, "some-host", sub.Address()) - } - - httpClient := initializeMockHTTPClient(&mockTransport{}) - Push(d, ":invalidurl", httpClient)(w, req) - assert.Equal(t, http.StatusInternalServerError, w.Code) -} - -func TestClientErrorByRequestingValidatingApiKey(t *testing.T) { - d := new(MockDispatcher) - - d.On("Register", mock.AnythingOfType("*dispatcher.standardSubscriber")).Return() - d.On("Close", mock.AnythingOfType("*dispatcher.standardSubscriber")).Return() - - w := NewStreamResponseRecorder() - req, err := http.NewRequest("GET", "/content/notifications-push", nil) - if err != nil { - t.Fatal(err) - } - - req.Header.Set("X-Forwarded-For", "some-host, some-other-host-that-isnt-used") - req.Header.Set(apiKeyHeaderField, "some-api-key") - - start = func(sub dispatcher.Subscriber) { - sub.NotificationChannel() <- "hi" - time.Sleep(10 * time.Millisecond) - w.closer <- true - - assert.True(t, time.Now().After(sub.Since())) - assert.Equal(t, "some-host", sub.Address()) - } - - httpClient := initializeMockHTTPClient(&mockTransport{ - shouldReturnError: true, - }) - - Push(d, "http://dummy.ft.com", httpClient)(w, req) - assert.Equal(t, http.StatusInternalServerError, w.Code) + Push(d)(w, req) + assert.Equal(t, 500, w.Code) } type MockDispatcher struct { @@ -297,32 +130,3 @@ type StreamResponseRecorder struct { func (r *StreamResponseRecorder) CloseNotify() <-chan bool { return r.closer } - -type MockWebClient struct{} -type mockTransport struct { - responseStatusCode int - responseBody string - shouldReturnError bool -} - -func initializeMockHTTPClient(tr *mockTransport) *http.Client { - client := http.DefaultClient - client.Transport = tr - return client -} - -func (t *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) { - response := &http.Response{ - Header: make(http.Header), - Request: req, - StatusCode: t.responseStatusCode, - } - - response.Header.Set("Content-Type", "application/json") - response.Body = ioutil.NopCloser(strings.NewReader(t.responseBody)) - - if t.shouldReturnError { - return nil, errors.New("Client error") - } - return response, nil -} diff --git a/resources/validator.go b/resources/validator.go deleted file mode 100644 index 54f645e..0000000 --- a/resources/validator.go +++ /dev/null @@ -1,52 +0,0 @@ -package resources - -import ( - log "github.com/Sirupsen/logrus" - "io" - "io/ioutil" - "net/http" -) - -func isValidApiKey(providedApiKey string, masheryApiKeyValidationURL string, httpClient *http.Client) (bool, string, int) { - if providedApiKey == "" { - return false, "Empty api key", http.StatusUnauthorized - } - - req, err := http.NewRequest("GET", masheryApiKeyValidationURL, nil) - if err != nil { - log.WithField("url", masheryApiKeyValidationURL).WithError(err).Error("Invalid URL for api key validation") - return false, "Invalid URL", http.StatusInternalServerError - } - - req.Header.Set(apiKeyHeaderField, providedApiKey) - - //if the api key has more than four characters we want to log the first four - apiKeyFirstChars := "" - if len(providedApiKey) > 4 { - apiKeyFirstChars = providedApiKey[0:4] - } - log.WithField("url", req.URL.String()).WithField("apiKeyFirstChars", apiKeyFirstChars).Info("Calling Mashery to validate api key") - - resp, err := httpClient.Do(req) - if err != nil { - log.WithField("url", req.URL.String()).WithError(err).Error("Cannot send request to Mashery") - return false, "Request to validate api key failed", http.StatusInternalServerError - } - defer func() { - io.Copy(ioutil.Discard, resp.Body) - resp.Body.Close() - }() - - respStatusCode := resp.StatusCode - if respStatusCode == http.StatusOK { - return true, "", 0 - } - - if respStatusCode == http.StatusUnauthorized { - log.WithField("apiKeyFirstChars", apiKeyFirstChars).Error("Invalid api key") - return false, "Invalid api key", http.StatusUnauthorized - } - - log.WithField("url", req.URL.String()).Errorf("Received unexpected status code from Mashery: %d", respStatusCode) - return false, "Request to validate api key returned an unexpected response", http.StatusServiceUnavailable -} diff --git a/vendor/vendor.json b/vendor/vendor.json deleted file mode 100644 index 82a9be6..0000000 --- a/vendor/vendor.json +++ /dev/null @@ -1,112 +0,0 @@ -{ - "comment": "", - "ignore": "test", - "package": [ - { - "checksumSHA1": "4gy/7n/lgtdi5efuHXwFcIe1ZI8=", - "path": "github.com/Financial-Times/go-fthealth/v1a", - "revision": "e7ccca038327a0091303a52445ec1f0fb66cb97b", - "revisionTime": "2017-05-25T09:50:41Z" - }, - { - "checksumSHA1": "v+iZMRghLJr+LHe8ihEViOFE+9g=", - "path": "github.com/Financial-Times/message-queue-gonsumer/consumer", - "revision": "6f96a5cb1e34c4baa8bc64f56524a7f2e0092ed3", - "revisionTime": "2017-06-22T11:17:49Z" - }, - { - "checksumSHA1": "lQfsRf7gWYQDNoI3IOZuwNGUwRo=", - "path": "github.com/Financial-Times/service-status-go/buildinfo", - "revision": "3f5199736a3d7ae52394c63aac36834786825e21", - "revisionTime": "2016-03-23T11:15:42Z" - }, - { - "checksumSHA1": "7QAsTdXi/6nTkDhqKy54YIc69d4=", - "path": "github.com/Financial-Times/service-status-go/gtg", - "revision": "3f5199736a3d7ae52394c63aac36834786825e21", - "revisionTime": "2016-03-23T11:15:42Z" - }, - { - "checksumSHA1": "YxgjuCI4TJyfQujUeQk3V+gypzo=", - "path": "github.com/Financial-Times/service-status-go/httphandlers", - "revision": "3f5199736a3d7ae52394c63aac36834786825e21", - "revisionTime": "2016-03-23T11:15:42Z" - }, - { - "checksumSHA1": "s7QDtBhIY9GOfV4gMGlIq/MXuuo=", - "path": "github.com/Sirupsen/logrus", - "revision": "7f976d3a76720c4c27af2ba716b85d2e0a7e38b1", - "revisionTime": "2017-07-10T14:32:56Z" - }, - { - "checksumSHA1": "DuEyF75v9xaKXfJsCPRdHNOpGZk=", - "origin": "github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew", - "path": "github.com/davecgh/go-spew/spew", - "revision": "f6abca593680b2315d2075e0f5e2a9751e3f431a", - "revisionTime": "2017-06-01T20:57:54Z" - }, - { - "checksumSHA1": "IkPM2QLv9urri7S49wzroQx9oXA=", - "path": "github.com/gorilla/context", - "revision": "08b5f424b9271eedf6f9f0ce86cb9396ed337a42", - "revisionTime": "2016-08-17T18:46:32Z" - }, - { - "checksumSHA1": "qvsEhA+BntKV5nqjCARYL2gLSzo=", - "path": "github.com/gorilla/mux", - "revision": "ac112f7d75a0714af1bd86ab17749b31f7809640", - "revisionTime": "2017-07-03T15:07:09Z" - }, - { - "checksumSHA1": "KJqRW8jfPoHquMAd6FI7x92JxFs=", - "path": "github.com/hashicorp/go-version", - "revision": "03c5bf6be031b6dd45afec16b1cf94fc8938bc77", - "revisionTime": "2017-02-02T08:07:59Z" - }, - { - "checksumSHA1": "CRYmanmmriS4QPTxS4mM9KhcZI0=", - "path": "github.com/jawher/mow.cli", - "revision": "8327d12beb75e6471b7f045588acc318d1147146", - "revisionTime": "2017-04-30T13:52:12Z" - }, - { - "checksumSHA1": "+oyIJwPyeof36XCkY8awrNfxaNM=", - "origin": "github.com/stretchr/testify/vendor/github.com/pmezard/go-difflib/difflib", - "path": "github.com/pmezard/go-difflib/difflib", - "revision": "f6abca593680b2315d2075e0f5e2a9751e3f431a", - "revisionTime": "2017-06-01T20:57:54Z" - }, - { - "checksumSHA1": "HUXE+Nrcau8FSaVEvPYHMvDjxOE=", - "path": "github.com/satori/go.uuid", - "revision": "5bf94b69c6b68ee1b541973bb8e1144db23a194b", - "revisionTime": "2017-03-21T23:07:31Z" - }, - { - "checksumSHA1": "iDI3Ec9Co5dn9MAf6VRg5cGNRPI=", - "origin": "github.com/stretchr/testify/vendor/github.com/stretchr/objx", - "path": "github.com/stretchr/objx", - "revision": "f6abca593680b2315d2075e0f5e2a9751e3f431a", - "revisionTime": "2017-06-01T20:57:54Z" - }, - { - "checksumSHA1": "A4Px2X/a0JBJfecp+vMiXE8IWFQ=", - "path": "github.com/stretchr/testify/assert", - "revision": "f6abca593680b2315d2075e0f5e2a9751e3f431a", - "revisionTime": "2017-06-01T20:57:54Z" - }, - { - "checksumSHA1": "q+MPhXUlpfmP5UgmOySPeldXwvs=", - "path": "github.com/stretchr/testify/mock", - "revision": "f6abca593680b2315d2075e0f5e2a9751e3f431a", - "revisionTime": "2017-06-01T20:57:54Z" - }, - { - "checksumSHA1": "oS0UZOGWVVsVciOQ8nZJ582mFF8=", - "path": "golang.org/x/sys/unix", - "revision": "9ccfe848b9db8435a24c424abbc07a921adf1df5", - "revisionTime": "2017-04-27T03:54:25Z" - } - ], - "rootPath": "github.com/Financial-Times/notifications-push" -}