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