From a92dd933bad2b4ba7bf1b709dceb7c828a293d55 Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Wed, 21 Nov 2018 17:06:21 -0800 Subject: [PATCH 1/2] receive docker hub notification --- docker/docker.go | 131 +++++++++++++++++++ docker/docker_test.go | 99 ++++++++++++++ testdata/docker/docker_hub_build_notice.json | 30 +++++ 3 files changed, 260 insertions(+) create mode 100644 docker/docker.go create mode 100644 docker/docker_test.go create mode 100644 testdata/docker/docker_hub_build_notice.json diff --git a/docker/docker.go b/docker/docker.go new file mode 100644 index 0000000..8d66c3a --- /dev/null +++ b/docker/docker.go @@ -0,0 +1,131 @@ +package docker + +// this package recieves the Docker Hub Automated Build webhook +// https://docs.docker.com/docker-hub/webhooks/ +// NOT the Docker Trusted Registry webhook +// https://docs.docker.com/ee/dtr/user/create-and-manage-webhooks/ + +import ( + "encoding/json" + "errors" + "io" + "io/ioutil" + "net/http" + + log "github.com/Sirupsen/logrus" +) + +// parse errors +var ( + ErrInvalidHTTPMethod = errors.New("invalid HTTP Method") + ErrParsingPayload = errors.New("error parsing payload") +) + +// Event defines a GitHub hook event type +type Event string + +// GitHub hook types +const ( + BuildEvent Event = "build" +) + +// BuildPayload a docker hub build notice +// https://docs.docker.com/docker-hub/webhooks/ +type BuildPayload struct { + CallbackURL string `json:"callback_url"` + PushData struct { + Images []string `json:"images"` + PushedAt float32 `json:"pushed_at"` + Pusher string `json:"pusher"` + Tag string `json:"tag"` + } `json:"push_data"` + Repository struct { + CommentCount int `json:"comment_count"` + DateCreated float32 `json:"date_created"` + Description string `json:"description"` + Dockerfile string `json:"dockerfile"` + FullDescription string `json:"full_description"` + IsOfficial bool `json:"is_official"` + IsPrivate bool `json:"is_private"` + IsTrusted bool `json:"is_trusted"` + Name string `json:"name"` + Namespace string `json:"namespace"` + Owner string `json:"owner"` + RepoName string `json:"repo_name"` + RepoURL string `json:"repo_url"` + StarCount int `json:"star_count"` + Status string `json:"status"` + } `json:"repository"` +} + +// there are no options for docker webhooks +// however I'm leaving this here for now in anticipation of future support for Docker Trusted Registry + +// // Option is a configuration option for the webhook +// type Option func(*Webhook) error + +// // Options is a namespace var for configuration options +// var Options = WebhookOptions{} + +// // WebhookOptions is a namespace for configuration option methods +// type WebhookOptions struct{} + +// // Secret registers the GitHub secret +// func (WebhookOptions) Secret(secret string) Option { +// return func(hook *Webhook) error { +// hook.secret = secret +// return nil +// } +// } + +// Webhook instance contains all methods needed to process events +type Webhook struct { + secret string +} + +// New creates and returns a WebHook instance denoted by the Provider type +func New() (*Webhook, error) { + // func New(options ...Option) (*Webhook, error) { + hook := new(Webhook) + // for _, opt := range options { + // if err := opt(hook); err != nil { + // return nil, errors.New("Error applying Option") + // } + // } + return hook, nil +} + +// Parse verifies and parses the events specified and returns the payload object or an error +func (hook Webhook) Parse(r *http.Request, events ...Event) (interface{}, error) { + defer func() { + _, _ = io.Copy(ioutil.Discard, r.Body) + _ = r.Body.Close() + }() + + if r.Method != http.MethodPost { + return nil, ErrInvalidHTTPMethod + } + + // event := r.Header.Get("X-GitHub-Event") + // if event == "" { + // return nil, ErrMissingGithubEventHeader + // } + // gitHubEvent := Event(event) + + payload, err := ioutil.ReadAll(r.Body) + if err != nil || len(payload) == 0 { + log.Error(ErrParsingPayload) + log.Error(err) + return nil, ErrParsingPayload + } + + var pl BuildPayload + err = json.Unmarshal([]byte(payload), &pl) + if err != nil { + log.Error(ErrParsingPayload) + log.Error(err) + return nil, ErrParsingPayload + } + return pl, err + +} diff --git a/docker/docker_test.go b/docker/docker_test.go new file mode 100644 index 0000000..e79f053 --- /dev/null +++ b/docker/docker_test.go @@ -0,0 +1,99 @@ +package docker + +import ( + "log" + "net/http" + "net/http/httptest" + "os" + "testing" + + "reflect" + + "github.com/stretchr/testify/require" +) + +// NOTES: +// - Run "go test" to run tests +// - Run "gocov test | gocov report" to report on test converage by file +// - Run "gocov test | gocov annotate -" to report on all code and functions, those ,marked with "MISS" were never called +// +// or +// +// -- may be a good idea to change to output path to somewherelike /tmp +// go test -coverprofile cover.out && go tool cover -html=cover.out -o cover.html +// + +const ( + path = "/webhooks" +) + +var hook *Webhook + +func TestMain(m *testing.M) { + + // setup + var err error + hook, err = New() + if err != nil { + log.Fatal(err) + } + os.Exit(m.Run()) + // teardown +} + +func newServer(handler http.HandlerFunc) *httptest.Server { + mux := http.NewServeMux() + mux.HandleFunc(path, handler) + return httptest.NewServer(mux) +} + +func TestWebhooks(t *testing.T) { + assert := require.New(t) + tests := []struct { + name string + event Event + typ interface{} + filename string + headers http.Header + }{ + { + name: "BuildEvent", + event: BuildEvent, + typ: BuildPayload{}, + filename: "../testdata/docker/docker_hub_build_notice.json", + headers: http.Header{ + "X-Github-Event": []string{"commit_comment"}, + }, + }, + } + + for _, tt := range tests { + tc := tt + client := &http.Client{} + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + payload, err := os.Open(tc.filename) + assert.NoError(err) + defer func() { + _ = payload.Close() + }() + + var parseError error + var results interface{} + server := newServer(func(w http.ResponseWriter, r *http.Request) { + results, parseError = hook.Parse(r, tc.event) + }) + defer server.Close() + req, err := http.NewRequest(http.MethodPost, server.URL+path, payload) + assert.NoError(err) + req.Header = tc.headers + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + assert.NoError(err) + assert.Equal(http.StatusOK, resp.StatusCode) + assert.NoError(parseError) + assert.Equal(reflect.TypeOf(tc.typ), reflect.TypeOf(results)) + }) + } +} diff --git a/testdata/docker/docker_hub_build_notice.json b/testdata/docker/docker_hub_build_notice.json new file mode 100644 index 0000000..589d4f7 --- /dev/null +++ b/testdata/docker/docker_hub_build_notice.json @@ -0,0 +1,30 @@ +{ + "callback_url": "https://registry.hub.docker.com/u/svendowideit/testhook/hook/2141b5bi5i5b02bec211i4eeih0242eg11000a/", + "push_data": { + "images": [ + "27d47432a69bca5f2700e4dff7de0388ed65f9d3fb1ec645e2bc24c223dc1cc3", + "51a9c7c1f8bb2fa19bcd09789a34e63f35abb80044bc10196e304f6634cc582c", + "..." + ], + "pushed_at": 1.417566161e+09, + "pusher": "trustedbuilder", + "tag": "latest" + }, + "repository": { + "comment_count": 0, + "date_created": 1.417494799e+09, + "description": "", + "dockerfile": "#\n# BUILD\u0009\u0009docker build -t svendowideit/apt-cacher .\n# RUN\u0009\u0009docker run -d -p 3142:3142 -name apt-cacher-run apt-cacher\n#\n# and then you can run containers with:\n# \u0009\u0009docker run -t -i -rm -e http_proxy http://192.168.1.2:3142/ debian bash\n#\nFROM\u0009\u0009ubuntu\n\n\nVOLUME\u0009\u0009[/var/cache/apt-cacher-ng]\nRUN\u0009\u0009apt-get update ; apt-get install -yq apt-cacher-ng\n\nEXPOSE \u0009\u00093142\nCMD\u0009\u0009chmod 777 /var/cache/apt-cacher-ng ; /etc/init.d/apt-cacher-ng start ; tail -f /var/log/apt-cacher-ng/*\n", + "full_description": "Docker Hub based automated build from a GitHub repo", + "is_official": false, + "is_private": true, + "is_trusted": true, + "name": "testhook", + "namespace": "svendowideit", + "owner": "svendowideit", + "repo_name": "svendowideit/testhook", + "repo_url": "https://registry.hub.docker.com/u/svendowideit/testhook/", + "star_count": 0, + "status": "Active" + } +} \ No newline at end of file From f1f5db7261e6562d09dbc7fc90de601990368dbe Mon Sep 17 00:00:00 2001 From: Benjamin Foote Date: Wed, 28 Nov 2018 11:11:43 -0800 Subject: [PATCH 2/2] #53 removing logging, remove extraneous comments --- docker/docker.go | 44 +++---------------------------------------- docker/docker_test.go | 4 ---- 2 files changed, 3 insertions(+), 45 deletions(-) diff --git a/docker/docker.go b/docker/docker.go index 8d66c3a..84de0ec 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -11,8 +11,6 @@ import ( "io" "io/ioutil" "net/http" - - log "github.com/Sirupsen/logrus" ) // parse errors @@ -21,10 +19,10 @@ var ( ErrParsingPayload = errors.New("error parsing payload") ) -// Event defines a GitHub hook event type +// Event defines a Docker hook event type type Event string -// GitHub hook types +// Docker hook types (only one for now) const ( BuildEvent Event = "build" ) @@ -58,40 +56,14 @@ type BuildPayload struct { } `json:"repository"` } -// there are no options for docker webhooks -// however I'm leaving this here for now in anticipation of future support for Docker Trusted Registry - -// // Option is a configuration option for the webhook -// type Option func(*Webhook) error - -// // Options is a namespace var for configuration options -// var Options = WebhookOptions{} - -// // WebhookOptions is a namespace for configuration option methods -// type WebhookOptions struct{} - -// // Secret registers the GitHub secret -// func (WebhookOptions) Secret(secret string) Option { -// return func(hook *Webhook) error { -// hook.secret = secret -// return nil -// } -// } - // Webhook instance contains all methods needed to process events type Webhook struct { secret string } -// New creates and returns a WebHook instance denoted by the Provider type +// New creates and returns a WebHook instance func New() (*Webhook, error) { - // func New(options ...Option) (*Webhook, error) { hook := new(Webhook) - // for _, opt := range options { - // if err := opt(hook); err != nil { - // return nil, errors.New("Error applying Option") - // } - // } return hook, nil } @@ -106,24 +78,14 @@ func (hook Webhook) Parse(r *http.Request, events ...Event) (interface{}, error) return nil, ErrInvalidHTTPMethod } - // event := r.Header.Get("X-GitHub-Event") - // if event == "" { - // return nil, ErrMissingGithubEventHeader - // } - // gitHubEvent := Event(event) - payload, err := ioutil.ReadAll(r.Body) if err != nil || len(payload) == 0 { - log.Error(ErrParsingPayload) - log.Error(err) return nil, ErrParsingPayload } var pl BuildPayload err = json.Unmarshal([]byte(payload), &pl) if err != nil { - log.Error(ErrParsingPayload) - log.Error(err) return nil, ErrParsingPayload } return pl, err diff --git a/docker/docker_test.go b/docker/docker_test.go index e79f053..e0890ab 100644 --- a/docker/docker_test.go +++ b/docker/docker_test.go @@ -61,9 +61,6 @@ func TestWebhooks(t *testing.T) { event: BuildEvent, typ: BuildPayload{}, filename: "../testdata/docker/docker_hub_build_notice.json", - headers: http.Header{ - "X-Github-Event": []string{"commit_comment"}, - }, }, } @@ -86,7 +83,6 @@ func TestWebhooks(t *testing.T) { defer server.Close() req, err := http.NewRequest(http.MethodPost, server.URL+path, payload) assert.NoError(err) - req.Header = tc.headers req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req)