Skip to content

Commit

Permalink
Merge pull request #28 from garethjevans/storage
Browse files Browse the repository at this point in the history
chore: use storage interface for handling hooks
  • Loading branch information
garethjevans authored Mar 1, 2021
2 parents a7de0f3 + 25267c2 commit 3d5f915
Show file tree
Hide file tree
Showing 10 changed files with 243 additions and 117 deletions.
1 change: 1 addition & 0 deletions charts/captain-hook/templates/serviceaccount.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ rules:
- "list"
- "create"
- "delete"
- "update"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.15

require (
github.com/cenkalti/backoff v2.2.1+incompatible
github.com/google/uuid v1.2.0 // indirect
github.com/gorilla/mux v1.8.0
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.8.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I=
Expand Down
123 changes: 21 additions & 102 deletions pkg/hook/handler.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package hook

import (
"bytes"
"crypto/tls"
"fmt"
"io"
"io/ioutil"
Expand All @@ -13,10 +11,8 @@ import (

"github.com/garethjevans/captain-hook/pkg/store"

"github.com/cenkalti/backoff"
"github.com/garethjevans/captain-hook/pkg/version"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)

Expand All @@ -25,30 +21,29 @@ const (
)

var (
defaultMaxRetryDuration = 45 * time.Second
defaultMaxRetryDuration = 10 * time.Second
)

// Options struct containing all options.
type Options struct {
Path string
Version string
ForwardURL string
InsecureRelay bool
client *http.Client
maxRetryDuration *time.Duration
store store.Store
Path string
Version string
ForwardURL string
handler *handler
}

// NewHook create a new hook handler.
func NewHook() (*Options, error) {
logrus.Infof("creating new webhook listener")
return &Options{
Path: os.Getenv("HOOK_PATH"),
ForwardURL: os.Getenv("FORWARD_URL"),
InsecureRelay: os.Getenv("INSECURE_RELAY") == "true",
Version: version.Version,
maxRetryDuration: &defaultMaxRetryDuration,
store: store.NewKubernetesStore(),
Path: os.Getenv("HOOK_PATH"),
Version: version.Version,
ForwardURL: os.Getenv("FORWARD_URL"),
handler: &handler{
InsecureRelay: os.Getenv("INSECURE_RELAY") == "true",
maxRetryDuration: &defaultMaxRetryDuration,
store: store.NewKubernetesStore(),
},
}, nil
}

Expand Down Expand Up @@ -130,8 +125,8 @@ func (o *Options) handleWebHookRequests(w http.ResponseWriter, r *http.Request)

func (o *Options) onGeneralHook(bodyBytes []byte, headers http.Header) error {
// Set a default max retry duration of 30 seconds if it's not set.
if o.maxRetryDuration == nil {
o.maxRetryDuration = &defaultMaxRetryDuration
if o.handler.maxRetryDuration == nil {
o.handler.maxRetryDuration = &defaultMaxRetryDuration
}

githubDeliveryEvent := headers.Get("X-Github-Delivery")
Expand All @@ -141,95 +136,19 @@ func (o *Options) onGeneralHook(bodyBytes []byte, headers http.Header) error {
// log.WithError(err).Errorf("unable to decode hmac")
//}

// store in db
err := o.store.StoreHook(o.ForwardURL, string(bodyBytes), headers)
if err != nil {
logrus.Errorf("failed to store webhook: %s", err)
return err
hook := Hook{
ForwardURL: o.ForwardURL,
Body: bodyBytes,
Headers: headers,
}

err = o.retryWebhookDelivery(o.ForwardURL, bodyBytes, headers)
err := o.handler.Handle(&hook)
if err != nil {
logrus.Errorf("failed to deliver webhook after %s, %s", o.maxRetryDuration, err)
logrus.Errorf("failed to deliver webhook after %s, %s", o.handler.maxRetryDuration, err)
return err
}

logrus.Infof("webhook delivery ok for %s", githubDeliveryEvent)

return nil
}

func (o *Options) retryWebhookDelivery(forwardURL string, bodyBytes []byte, header http.Header) error {
f := func() error {
logrus.Debugf("relaying %s", string(bodyBytes))
//g := hmac.NewGenerator("sha256", decodedHmac)
//signature := g.HubSignature(bodyBytes)

var httpClient *http.Client

if o.client != nil {
httpClient = o.client
} else {
if o.InsecureRelay {
// #nosec G402
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}

httpClient = &http.Client{Transport: tr}
} else {
httpClient = &http.Client{}
}
}

req, err := http.NewRequest("POST", forwardURL, bytes.NewReader(bodyBytes))
if err != nil {
return err
}
req.Header = header

// does this need to be resigned?
//req.Header.Add("X-Hub-Signature", signature)

resp, err := httpClient.Do(req)
if err != nil {
return err
}

logrus.Infof("got resp code %d from url '%s'", resp.StatusCode, forwardURL)

// If we got a 500, check if it's got the "repository not configured" string in the body. If so, we retry.
if resp.StatusCode == 500 {
respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 10000000))
if err != nil {
return backoff.Permanent(errors.Wrap(err, "parsing resp.body"))
}
err = resp.Body.Close()
if err != nil {
return backoff.Permanent(errors.Wrap(err, "closing resp.body"))
}
logrus.Infof("got error respBody '%s'", string(respBody))
}

// If we got anything other than a 2xx, retry as well.
// We're leaving this distinct from the "not configured" behavior in case we want to resurrect that later. (apb)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return errors.Errorf("%s not available, error was %s", req.URL.String(), resp.Status)
}

// And finally, if we haven't gotten any errors, just return nil because we're good.
return nil
}

bo := backoff.NewExponentialBackOff()
// Try again after 2/4/8/... seconds if necessary, for up to 90 seconds, may take up to a minute to for the secret to replicate
bo.InitialInterval = 2 * time.Second
bo.MaxElapsedTime = 2 * (*o.maxRetryDuration)
bo.Reset()

return backoff.RetryNotify(f, bo, func(e error, t time.Duration) {
logrus.Infof("webhook relaying failed: %s, backing off for %s", e, t)
})
}
8 changes: 5 additions & 3 deletions pkg/hook/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ func TestWebhooks(t *testing.T) {

retryDuration := 5 * time.Second
handler := Options{
maxRetryDuration: &retryDuration,
store: store.NewLoggingStore(),
handler: &handler{
maxRetryDuration: &retryDuration,
store: store.NewLoggingStore(),
},
}

attempts := 0
Expand All @@ -89,7 +91,7 @@ func TestWebhooks(t *testing.T) {
defer server.Close()

handler.ForwardURL = server.URL
handler.client = server.Client()
handler.handler.client = server.Client()

w := NewFakeRespone(t)
handler.handleWebHookRequests(w, r)
Expand Down
126 changes: 126 additions & 0 deletions pkg/hook/store_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package hook

import (
"bytes"
"crypto/tls"
"io"
"io/ioutil"
"net/http"
"time"

"github.com/cenkalti/backoff"
"github.com/garethjevans/captain-hook/pkg/store"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)

type handler struct {
ForwardURL string
InsecureRelay bool
client *http.Client
maxRetryDuration *time.Duration
store store.Store
}

func (h *handler) Handle(hook *Hook) error {
// need to have a think about that the logic would be here.
hookID, err := h.store.StoreHook(hook.ForwardURL, hook.Body, hook.Headers)
if err != nil {
return err
}

hook.ID = hookID

// attempt to send
err = h.send(hook.ForwardURL, hook.Body, hook.Headers)
if err != nil {
// if failed, mark as failed with the error as the message
err = h.store.Error(hookID, err.Error())
if err != nil {
return err
}
}

// if success, mark as successful,
err = h.store.Success(hookID)
if err != nil {
return err
}

return nil
}

func (h *handler) send(forwardURL string, bodyBytes []byte, header http.Header) error {
f := func() error {
logrus.Debugf("relaying %s", string(bodyBytes))
//g := hmac.NewGenerator("sha256", decodedHmac)
//signature := g.HubSignature(bodyBytes)

var httpClient *http.Client

if h.client != nil {
httpClient = h.client
} else {
if h.InsecureRelay {
// #nosec G402
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}

httpClient = &http.Client{Transport: tr}
} else {
httpClient = &http.Client{}
}
}

req, err := http.NewRequest("POST", forwardURL, bytes.NewReader(bodyBytes))
if err != nil {
return err
}
req.Header = header

// does this need to be resigned?
//req.Header.Add("X-Hub-Signature", signature)

resp, err := httpClient.Do(req)
if err != nil {
return err
}

logrus.Infof("got resp code %d from url '%s'", resp.StatusCode, forwardURL)

// If we got a 500, check if it's got the "repository not configured" string in the body. If so, we retry.
if resp.StatusCode == 500 {
respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 10000000))
if err != nil {
return backoff.Permanent(errors.Wrap(err, "parsing resp.body"))
}
err = resp.Body.Close()
if err != nil {
return backoff.Permanent(errors.Wrap(err, "closing resp.body"))
}
logrus.Infof("got error respBody '%s'", string(respBody))
}

// If we got anything other than a 2xx, retry as well.
// We're leaving this distinct from the "not configured" behavior in case we want to resurrect that later. (apb)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return errors.Errorf("%s not available, error was %s", req.URL.String(), resp.Status)
}

// And finally, if we haven't gotten any errors, just return nil because we're good.
return nil
}

bo := backoff.NewExponentialBackOff()
// Try again after 2/4/8/... seconds if necessary, for up to 90 seconds, may take up to a minute to for the secret to replicate
bo.InitialInterval = 2 * time.Second
bo.MaxElapsedTime = 2 * (*h.maxRetryDuration)
bo.Reset()

return backoff.RetryNotify(f, bo, func(e error, t time.Duration) {
logrus.Infof("webhook relaying failed: %s, backing off for %s", e, t)
})
}
11 changes: 11 additions & 0 deletions pkg/hook/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package hook

// Hook struct to hold everything related to a hook.
type Hook struct {
ID string
ForwardURL string
Headers map[string][]string
Body []byte
// Status? State?

}
10 changes: 7 additions & 3 deletions pkg/store/interface.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package store

import "net/http"

// Store interface to implement a storage strategy.
type Store interface {
// StoreHook stores a webhook in the store.
StoreHook(forwardURL string, body string, header http.Header) error
StoreHook(forwardURL string, body []byte, headers map[string][]string) (string, error)

// Success marks a hook as successful.
Success(id string) error

// Marks a hook as error, with the error message.
Error(id string, message string) error
}
Loading

0 comments on commit 3d5f915

Please sign in to comment.