diff --git a/docs/topics/genericwebhook.md b/docs/topics/genericwebhook.md new file mode 100644 index 000000000..2bf9c07de --- /dev/null +++ b/docs/topics/genericwebhook.md @@ -0,0 +1,65 @@ +# Generic Webhook + +Brigade contains a Generic Webhook which is part of its API Server and can be used to accept requests from other platforms or systems. + +Generic Webhook is _not enabled by default_. + +## Intro to Generic Webhook + +Brigade API Server can optionally be activated to accept `POST` webhook requests at `/webhook/:projectID/:secret` path. When this endpoint is called, Brigade will respond by creating a Build with a `webhook` event. This provides Brigade developers with the ability to trigger scripts based on messages received from any platform that can send a POST HTTP request. + +## Configuring Brigade API Server for Generic Webhook + +Generic Webhook support is disabled by default, but can easily be turned on during installation or upgrade of Brigade: + +``` +$ helm install -n brigade brigade/brigade --set genericGateway.enabled=true +``` + +This will enable the Generic Webhook on Brigade API Server. However, be aware that Brigade API Server is not exposed outside the cluster. In case you are certain from a security perspective that you want to expose the entire API Server via a [Kubernetes LoadBalancer Service](https://kubernetes.io/docs/concepts/services-networking/#loadbalancer), you can install Brigade via the following command: + +``` +$ helm install -n brigade brigade/brigade --set genericGateway.enabled=true,api.service.type=LoadBalancer +``` + +Alternatively, for enhanced security, you can install an SSL proxy (like `cert-manager`) and direct it to the internal API Server service. + +## Using the Generic Webhook + +As mentioned, Generic Webhook accepts POST requests at `/webhook/:projectID/:secret` endpoint. These requests should also carry a JSON payload. + +- `projectID` is the Brigade Project ID +- `secret` is a custom secret for this specific project's Generic Webhook support. You can think of it as a simple authentication mechanism for this Project's Generic Webhook support. Every Project has its own unique secret. + +When you create a new Brigade Project via Brig CLI, you can optionally create such a secret by adding a secret named `genericWebhookSecret`, containing your desired value. Alternatively, Brig will generate and output one for you. + +When calling the Generic Webhook endpoint, you can include a custom JSON payload such as this one: + +```json +{ + "ref": "refs/heads/changes", + "commit": "b60ad9543b2ddbbe73430dd6898b75883306cecc" +} +``` + +`Ref` and `commit` values would be used to configure the specific revision that Brigade will pull from your repository. + +Last but not least, here is a sample Brigade.js file that could be used as a base for your own scripts that respond to Generic Webhook's `webhook` event. This script will echo the name of your project and 'webhook'. + +```javascript +const { events, Job } = require("brigadier"); +events.on("webhook", (e, p) => { + var echo = new Job("echo", "alpine:3.8"); + echo.storage.enabled = false; + echo.tasks = [ + "echo Project " + p.name, + "echo Event $EVENT_NAME" + ]; + + echo.env = { + "EVENT_NAME": e.type + }; + + echo.run(); +}); +``` \ No newline at end of file diff --git a/docs/topics/index.md b/docs/topics/index.md index bbaa4b99e..4c7787de7 100644 --- a/docs/topics/index.md +++ b/docs/topics/index.md @@ -14,6 +14,7 @@ If you don't see a topic guide here and have a reasonable level of knowledge on - [Scripting Guide - Advanced](scripting_advanced.md): Advanced examples for `brigade.js` files. - [GitHub Integration](github.md): A guide for configuring GitHub integration. - [Container Registry Integration](dockerhub.md): A guide for configuring integration with DockerHub or Azure Container Registry. + - [Generic Webhook](genericwebhook.md): How to use Brigade's Generic Webhook functionality. - [Using Secrets](secrets.md): How to pass sensitive data into builds. - [Brigade Gateways](gateways.md): Learn how to write your own Brigade gateway. - Configuring and Running Brigade diff --git a/pkg/webhook/generic.go b/pkg/webhook/generic.go index 0634743bf..488a5e6ff 100644 --- a/pkg/webhook/generic.go +++ b/pkg/webhook/generic.go @@ -9,7 +9,7 @@ import ( "github.com/Azure/brigade/pkg/brigade" "github.com/Azure/brigade/pkg/storage" - "github.com/emicklei/go-restful" + restful "github.com/emicklei/go-restful" ) type genericWebhook struct { @@ -34,25 +34,6 @@ func (g *genericWebhook) Handle(request *restful.Request, response *restful.Resp projectID := request.PathParameter("projectID") secret := request.PathParameter("secret") - gwData := &genericWebhookData{} - err := request.ReadEntity(gwData) - - if err != nil { - log.Printf("Failed to read GenericWebHookData: %s", err) - response.WriteErrorString(http.StatusBadRequest, "{\"status\": \"Malformed genericWebhookData\"}") - return - } - - log.Printf("gwData: %#v\n", gwData) - - body, err := ioutil.ReadAll(request.Request.Body) - if err != nil { - log.Printf("Failed to read body: %s", err) - response.WriteErrorString(http.StatusBadRequest, "{\"status\": \"Malformed body\"}") - return - } - defer request.Request.Body.Close() - proj, err := g.store.GetProject(projectID) if err != nil { @@ -62,19 +43,39 @@ func (g *genericWebhook) Handle(request *restful.Request, response *restful.Resp } // if the secret is "" (probably due to a Brigade upgrade) - // refuse to serve it, so user will be forced to update it + // refuse to serve it, so Brigade admin will be forced to update the project with a non-empty secret if proj.GenericWebhookSecret == "" { log.Printf("Secret for project %s is empty, please update it and try again", projectID) - response.WriteErrorString(http.StatusUnauthorized, "{\"status\": \"secret is empty, please update it\"}") + response.WriteErrorString(http.StatusUnauthorized, "{\"status\": \"secret is empty, please update it and try again\"}") return } + // compare secrets if secret != proj.GenericWebhookSecret { log.Printf("Secret %s for project %s is wrong", secret, projectID) response.WriteErrorString(http.StatusUnauthorized, "{\"status\": \"secret is wrong\"}") return } + gwData := &genericWebhookData{} + err = request.ReadEntity(gwData) + + if err != nil { + log.Printf("Failed to read GenericWebHookData: %s", err) + response.WriteErrorString(http.StatusBadRequest, "{\"status\": \"Malformed genericWebhookData\"}") + return + } + + log.Printf("gwData: %#v\n", gwData) + + body, err := ioutil.ReadAll(request.Request.Body) + if err != nil { + log.Printf("Failed to read body: %s", err) + response.WriteErrorString(http.StatusBadRequest, "{\"status\": \"Malformed body\"}") + return + } + defer request.Request.Body.Close() + go g.notifyGenericWebhookEvent(proj, body, gwData) response.Write([]byte("{\"status\": \"Success\"}")) } @@ -86,7 +87,6 @@ func (g *genericWebhook) notifyGenericWebhookEvent(proj *brigade.Project, payloa } func (g *genericWebhook) genericWebhookEvent(proj *brigade.Project, payload []byte, gwData *genericWebhookData) error { - revision := &brigade.Revision{} revision.Commit = gwData.Commit revision.Ref = gwData.Ref diff --git a/pkg/webhook/generic_test.go b/pkg/webhook/generic_test.go index 5fdf5f597..5d8cbaa0d 100644 --- a/pkg/webhook/generic_test.go +++ b/pkg/webhook/generic_test.go @@ -10,7 +10,7 @@ import ( "github.com/Azure/brigade/pkg/storage" "github.com/Azure/brigade/pkg/storage/mock" - "github.com/emicklei/go-restful" + restful "github.com/emicklei/go-restful" ) func newTestGenericWebhookHandler(store storage.Store) *genericWebhook {