diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 6b5f3424..26644aaa 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -10,6 +10,10 @@ "ImportPath": "github.com/bitrise-io/go-utils/builtinutil", "Rev": "6eb582e08179a025ed2deb86ea6dffbd13ba1ff6" }, + { + "ImportPath": "github.com/bitrise-io/go-utils/colorstring", + "Rev": "6eb582e08179a025ed2deb86ea6dffbd13ba1ff6" + }, { "ImportPath": "github.com/bitrise-io/go-utils/pointers", "Rev": "6eb582e08179a025ed2deb86ea6dffbd13ba1ff6" @@ -29,8 +33,8 @@ }, { "ImportPath": "github.com/gorilla/mux", - "Comment": "v1.4.0-15-gbb285ea", - "Rev": "bb285ea687c5c77bb6935fdb2402b121d8efcbec" + "Comment": "v1.4.0-16-g24fca30", + "Rev": "24fca303ac6da784b9e8269f724ddeb0b2eea5e7" }, { "ImportPath": "github.com/pmezard/go-difflib/difflib", diff --git a/README.md b/README.md index a00e86bd..b46552e8 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,12 @@ If the (commit) message includes `[skip ci]` or `[ci skip]` no build will be tri * [Assembla](https://assembla.com) * handled on the path: `/h/assembla/BITRISE-APP-SLUG/BITRISE-APP-API-TOKEN` +Service independent: + +* Passthrough - reads the request headers and body and passes it to the triggered build as environment variables. + * handled on the path: `/h/passthrough/BITRISE-APP-SLUG/BITRISE-APP-API-TOKEN` + + ### GitHub - setup & usage: All you have to do is register your `bitrise-webhooks` URL for @@ -206,6 +212,45 @@ You can also send environment variables that will be available in your workflow An example with all parameters included: `workflow: primary|b: master|tag: v1.0|commit:eee55509f16e7715bdb43308bb55e8736da4e21e|m: start my build!|ENV[DEVICE_NAME]:iPhone 6S|ENV[DEVICE_UDID]:82667b4079914d4aabed9c216620da5dedab630a` +### Passthrough - setup & usage: + +Simply register or use the `.../h/passthrough/BITRISE-APP-SLUG/BITRISE-APP-API-TOKEN` url. +**Every** request received on the `passthrough` endpoint will trigger a build, __no filtering is done or supported!__. + +_The only limit is that neither the Headers nor the Body can be larger than 10kb._ + +The headers will be passed to the build in JSON serialized form, as the value of `BITRISE_WEBHOOK_PASSTHROUGH_HEADERS`. +Note: headers are key value maps where the value is an array or strings, not just a single string value! +Example: + +``` +{ + "Content-Type": [ + "application/json" + ], + "Some-Custom-Header-List": [ + "first-value", + "second-value" + ] +} +``` + +The body will be passed to the build as-it-is (in string/text form), as the value of `BITRISE_WEBHOOK_PASSTHROUGH_BODY`. + +Demo: run the server locally (e.g. with `bitrise run start`) and call the `.../h/passthrough/...` endpoint with `curl`: + +``` +curl -X POST --data 'just a text body' -H 'Example-Header: example header value' 'http://localhost:4000/h/passthrough/BITRISE-APP-SLUG/BITRISE-APP-API-TOKEN' +``` + +by default the server will print what and where it would send (debug mode), so you should see this in the server's log: + +``` +2017/09/10 16:30:18 ===> Triggering Build: (url:https://www.bitrise.io/app/BITRISE-APP-SLUG/build/start.json) +2017/09/10 16:30:18 ====> JSON body: {"build_params":{"branch":"master","environments":[{"mapped_to":"BITRISE_WEBHOOK_PASSTHROUGH_HEADERS","value":"{\"Accept\":[\"*/*\"],\"Accept-Encoding\":[\"gzip\"],\"Content-Length\":[\"16\"],\"Content-Type\":[\"application/x-www-form-urlencoded\"],\"Example-Header\":[\"example header value\"],\"User-Agent\":[\"curl/7.54.0\"],\"X-Forwarded-For\":[\"::1\"]}","is_expand":false},{"mapped_to":"BITRISE_WEBHOOK_PASSTHROUGH_BODY","value":"just a text body","is_expand":false}]},"triggered_by":"webhook"} +``` + + ## How to compile & run the server * Install [Go](https://golang.org), and [set up your Workspace](https://golang.org/doc/code.html#Workspaces) and your [$GOPATH](https://golang.org/doc/code.html#GOPATH) diff --git a/bitriseapi/bitriseapi.go b/bitriseapi/bitriseapi.go index 61469a92..4afcdfb8 100644 --- a/bitriseapi/bitriseapi.go +++ b/bitriseapi/bitriseapi.go @@ -10,13 +10,15 @@ import ( "net/http" "net/url" "time" + + "github.com/bitrise-io/go-utils/colorstring" ) // EnvironmentItem ... type EnvironmentItem struct { - Name string `json:"mapped_to,omitempty"` - Value string `json:"value,omitempty"` - IsExpand bool `json:"is_expand,omitempty"` + Name string `json:"mapped_to"` + Value string `json:"value"` + IsExpand bool `json:"is_expand"` } // BuildParamsModel ... @@ -102,8 +104,10 @@ func TriggerBuild(url *url.URL, apiToken string, params TriggerAPIParamsModel, i return TriggerAPIResponseModel{}, false, fmt.Errorf("TriggerBuild: failed to json marshal: %s", err) } - log.Printf("===> Triggering Build: (url:%s)", url) - log.Printf("====> JSON body: %s", jsonStr) + if isOnlyLog { + log.Println(colorstring.Yellowf("===> Triggering Build: (url:%s)", url)) + log.Println(colorstring.Yellowf("====> JSON body: %s", jsonStr)) + } if isOnlyLog { return TriggerAPIResponseModel{ diff --git a/service/hook/assembla/assembla_test.go b/service/hook/assembla/assembla_test.go index 65c4d04d..90fe1483 100644 --- a/service/hook/assembla/assembla_test.go +++ b/service/hook/assembla/assembla_test.go @@ -4,8 +4,8 @@ import ( "net/http" "testing" - "github.com/stretchr/testify/require" "github.com/bitrise-io/bitrise-webhooks/bitriseapi" + "github.com/stretchr/testify/require" "io/ioutil" "strings" ) @@ -14,7 +14,7 @@ func Test_detectContentType(t *testing.T) { t.Log("Push event - should handle") { header := http.Header{ - "Content-Type": {"application/json"}, + "Content-Type": {"application/json"}, } contentType, err := detectContentType(header) require.NoError(t, err) @@ -27,20 +27,20 @@ func Test_transformPushEvent(t *testing.T) { { pushEvent := PushEventModel{ SpaceEventModel: SpaceEventModel{ - Space: "Space name", + Space: "Space name", Action: "committed", Object: "Changeset", }, MessageEventModel: MessageEventModel{ - Title: "1 commits [branchname]", - Body: "ErikPoort pushed 1 commits [branchname]\n", + Title: "1 commits [branchname]", + Body: "ErikPoort pushed 1 commits [branchname]\n", Author: "ErikPoort", }, GitEventModel: GitEventModel{ RepositorySuffix: "origin", - RepositoryURL: "git@git.assembla.com:username/project.git", - Branch: "branchname", - CommitID: "sha1chars11", + RepositoryURL: "git@git.assembla.com:username/project.git", + Branch: "branchname", + CommitID: "sha1chars11", }, } @@ -71,20 +71,20 @@ func Test_incorrectPostOptions(t *testing.T) { { pushEvent := PushEventModel{ SpaceEventModel: SpaceEventModel{ - Space: "Space name", + Space: "Space name", Action: "committed", Object: "Changeset", }, MessageEventModel: MessageEventModel{ - Title: "1 commits [branchname]", - Body: "ErikPoort pushed 1 commits [branchname]\n", + Title: "1 commits [branchname]", + Body: "ErikPoort pushed 1 commits [branchname]\n", Author: "ErikPoort", }, GitEventModel: GitEventModel{ RepositorySuffix: "---", - RepositoryURL: "---", - Branch: "---", - CommitID: "---", + RepositoryURL: "---", + Branch: "---", + CommitID: "---", }, } @@ -101,20 +101,20 @@ func Test_emptyGitEventOptions(t *testing.T) { { pushEvent := PushEventModel{ SpaceEventModel: SpaceEventModel{ - Space: "Space name", + Space: "Space name", Action: "committed", Object: "Changeset", }, MessageEventModel: MessageEventModel{ - Title: "1 commits [branchname]", - Body: "ErikPoort pushed 1 commits [branchname]\n", + Title: "1 commits [branchname]", + Body: "ErikPoort pushed 1 commits [branchname]\n", Author: "ErikPoort", }, GitEventModel: GitEventModel{ RepositorySuffix: "", - RepositoryURL: "", - Branch: "", - CommitID: "", + RepositoryURL: "", + Branch: "", + CommitID: "", }, } @@ -172,7 +172,7 @@ func Test_HookProvider_TransformRequest(t *testing.T) { { request := http.Request{ Header: http.Header{ - "Content-Type": {"not/supported"}, + "Content-Type": {"not/supported"}, }, } hookTransformResult := provider.TransformRequest(&request) @@ -184,7 +184,7 @@ func Test_HookProvider_TransformRequest(t *testing.T) { { request := http.Request{ Header: http.Header{ - "Content-Type": {"application/json"}, + "Content-Type": {"application/json"}, }, } hookTransformResult := provider.TransformRequest(&request) @@ -196,7 +196,7 @@ func Test_HookProvider_TransformRequest(t *testing.T) { { request := http.Request{ Header: http.Header{ - "Content-Type": {"application/json"}, + "Content-Type": {"application/json"}, }, Body: ioutil.NopCloser(strings.NewReader(sampleCodePushData)), } @@ -224,11 +224,11 @@ func Test_IncorrectJSONData(t *testing.T) { { request := http.Request{ Header: http.Header{ - "Content-Type": {"application/json"}, + "Content-Type": {"application/json"}, }, Body: ioutil.NopCloser(strings.NewReader(sampleIncorrectJSONData)), } hookTransformResult := provider.TransformRequest(&request) require.Error(t, hookTransformResult.Error) } -} \ No newline at end of file +} diff --git a/service/hook/endpoint.go b/service/hook/endpoint.go index bc8acad0..be87681f 100644 --- a/service/hook/endpoint.go +++ b/service/hook/endpoint.go @@ -10,15 +10,17 @@ import ( "github.com/bitrise-io/bitrise-webhooks/config" "github.com/bitrise-io/bitrise-webhooks/metrics" "github.com/bitrise-io/bitrise-webhooks/service" + "github.com/bitrise-io/bitrise-webhooks/service/hook/assembla" "github.com/bitrise-io/bitrise-webhooks/service/hook/bitbucketv2" hookCommon "github.com/bitrise-io/bitrise-webhooks/service/hook/common" "github.com/bitrise-io/bitrise-webhooks/service/hook/deveo" "github.com/bitrise-io/bitrise-webhooks/service/hook/github" "github.com/bitrise-io/bitrise-webhooks/service/hook/gitlab" "github.com/bitrise-io/bitrise-webhooks/service/hook/gogs" + "github.com/bitrise-io/bitrise-webhooks/service/hook/passthrough" "github.com/bitrise-io/bitrise-webhooks/service/hook/slack" "github.com/bitrise-io/bitrise-webhooks/service/hook/visualstudioteamservices" - "github.com/bitrise-io/bitrise-webhooks/service/hook/assembla" + "github.com/bitrise-io/go-utils/colorstring" "github.com/gorilla/mux" ) @@ -32,6 +34,7 @@ func supportedProviders() map[string]hookCommon.Provider { "gogs": gogs.HookProvider{}, "deveo": deveo.HookProvider{}, "assembla": assembla.HookProvider{}, + "passthrough": passthrough.HookProvider{}, } } @@ -96,7 +99,7 @@ func triggerBuild(triggerURL *url.URL, apiToken string, triggerAPIParams bitrise log.Printf(" ===> trigger build: %s", triggerURL) isOnlyLog := !(config.SendRequestToURL != nil || config.GetServerEnvMode() == config.ServerEnvModeProd) if isOnlyLog { - log.Println(" (debug) isOnlyLog: true") + log.Println(colorstring.Yellow(" (debug) isOnlyLog: true")) } responseModel, isSuccess, err := bitriseapi.TriggerBuild(triggerURL, apiToken, triggerAPIParams, isOnlyLog) diff --git a/service/hook/passthrough/passthrough.go b/service/hook/passthrough/passthrough.go new file mode 100644 index 00000000..913fe871 --- /dev/null +++ b/service/hook/passthrough/passthrough.go @@ -0,0 +1,64 @@ +package passthrough + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + "github.com/bitrise-io/bitrise-webhooks/bitriseapi" + hookCommon "github.com/bitrise-io/bitrise-webhooks/service/hook/common" +) + +const ( + envKeyHeaders = `BITRISE_WEBHOOK_PASSTHROUGH_HEADERS` + maxHeaderSizeBytes = 10 * 1024 + envKeyBody = `BITRISE_WEBHOOK_PASSTHROUGH_BODY` + maxBodySizeBytes = 10 * 1024 +) + +// HookProvider ... +type HookProvider struct{} + +// TransformRequest ... +func (hp HookProvider) TransformRequest(r *http.Request) hookCommon.TransformResultModel { + headerAsJSON := []byte{} + if r.Header != nil { + b, err := json.Marshal(r.Header) + if err != nil { + return hookCommon.TransformResultModel{Error: fmt.Errorf("Failed to JSON serialize request headers: %s", err)} + } + headerAsJSON = b + } + if len(headerAsJSON) > maxHeaderSizeBytes { + return hookCommon.TransformResultModel{Error: fmt.Errorf("Headers too large, larger than %d bytes", maxHeaderSizeBytes)} + } + + bodyBytes := []byte{} + if r.Body != nil { + b, err := ioutil.ReadAll(r.Body) + if err != nil { + return hookCommon.TransformResultModel{Error: fmt.Errorf("Failed to get request body: %s", err)} + } + bodyBytes = b + } + if len(bodyBytes) > maxBodySizeBytes { + return hookCommon.TransformResultModel{Error: fmt.Errorf("Body too large, larger than %d bytes", maxBodySizeBytes)} + } + + environments := []bitriseapi.EnvironmentItem{ + bitriseapi.EnvironmentItem{Name: envKeyHeaders, Value: string(headerAsJSON), IsExpand: false}, + bitriseapi.EnvironmentItem{Name: envKeyBody, Value: string(bodyBytes), IsExpand: false}, + } + + return hookCommon.TransformResultModel{ + TriggerAPIParams: []bitriseapi.TriggerAPIParamsModel{ + { + BuildParams: bitriseapi.BuildParamsModel{ + Branch: "master", + Environments: environments, + }, + }, + }, + } +} diff --git a/service/hook/passthrough/passthrough_test.go b/service/hook/passthrough/passthrough_test.go new file mode 100644 index 00000000..9a82f613 --- /dev/null +++ b/service/hook/passthrough/passthrough_test.go @@ -0,0 +1,87 @@ +package passthrough + +import ( + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/bitrise-io/bitrise-webhooks/bitriseapi" + "github.com/stretchr/testify/require" +) + +func Test_HookProvider_TransformRequest(t *testing.T) { + provider := HookProvider{} + + t.Log("Empty headers & body") + { + request := http.Request{} + hookTransformResult := provider.TransformRequest(&request) + require.NoError(t, hookTransformResult.Error) + require.False(t, hookTransformResult.ShouldSkip) + require.Equal(t, []bitriseapi.TriggerAPIParamsModel{ + { + BuildParams: bitriseapi.BuildParamsModel{ + Branch: "master", + Environments: []bitriseapi.EnvironmentItem{ + bitriseapi.EnvironmentItem{Name: "BITRISE_WEBHOOK_PASSTHROUGH_HEADERS", Value: "", IsExpand: false}, + bitriseapi.EnvironmentItem{Name: "BITRISE_WEBHOOK_PASSTHROUGH_BODY", Value: "", IsExpand: false}, + }, + }, + }, + }, hookTransformResult.TriggerAPIParams) + require.Equal(t, false, hookTransformResult.DontWaitForTriggerResponse) + } + + t.Log("Request with headers & body") + { + bodyContent := `A simple, + +multi line body +content.` + request := http.Request{ + Header: http.Header{ + "Content-Type": {"application/json"}, + "Some-Custom-Header-List": {"first-value", "second-value"}, + }, + Body: ioutil.NopCloser(strings.NewReader(bodyContent)), + } + hookTransformResult := provider.TransformRequest(&request) + require.NoError(t, hookTransformResult.Error) + require.False(t, hookTransformResult.ShouldSkip) + require.Equal(t, []bitriseapi.TriggerAPIParamsModel{ + { + BuildParams: bitriseapi.BuildParamsModel{ + Branch: "master", + Environments: []bitriseapi.EnvironmentItem{ + bitriseapi.EnvironmentItem{Name: "BITRISE_WEBHOOK_PASSTHROUGH_HEADERS", Value: `{"Content-Type":["application/json"],"Some-Custom-Header-List":["first-value","second-value"]}`, IsExpand: false}, + bitriseapi.EnvironmentItem{Name: "BITRISE_WEBHOOK_PASSTHROUGH_BODY", Value: bodyContent, IsExpand: false}, + }, + }, + }, + }, hookTransformResult.TriggerAPIParams) + require.Equal(t, false, hookTransformResult.DontWaitForTriggerResponse) + } + + t.Log("Body too large") + { + request := http.Request{ + Body: ioutil.NopCloser(strings.NewReader(strings.Repeat("a", 10*1024+1))), + } + hookTransformResult := provider.TransformRequest(&request) + require.False(t, hookTransformResult.ShouldSkip) + require.EqualError(t, hookTransformResult.Error, "Body too large, larger than 10240 bytes") + } + + t.Log("Headers too large") + { + request := http.Request{ + Header: http.Header{ + "Some-Custom-Header-List": {"first-value", "second-value", strings.Repeat("a", 10*1024+1)}, + }, + } + hookTransformResult := provider.TransformRequest(&request) + require.False(t, hookTransformResult.ShouldSkip) + require.EqualError(t, hookTransformResult.Error, "Headers too large, larger than 10240 bytes") + } +} diff --git a/vendor/github.com/bitrise-io/go-utils/colorstring/colorstring.go b/vendor/github.com/bitrise-io/go-utils/colorstring/colorstring.go new file mode 100644 index 00000000..5f31fa92 --- /dev/null +++ b/vendor/github.com/bitrise-io/go-utils/colorstring/colorstring.go @@ -0,0 +1,110 @@ +package colorstring + +import ( + "fmt" +) + +// Color ... +// ANSI color escape sequences +type Color string + +const ( + blackColor Color = "\x1b[30;1m" + redColor Color = "\x1b[31;1m" + greenColor Color = "\x1b[32;1m" + yellowColor Color = "\x1b[33;1m" + blueColor Color = "\x1b[34;1m" + magentaColor Color = "\x1b[35;1m" + cyanColor Color = "\x1b[36;1m" + resetColor Color = "\x1b[0m" +) + +// ColorFunc ... +type ColorFunc func(a ...interface{}) string + +func addColor(color Color, msg string) string { + return string(color) + msg + string(resetColor) +} + +// NoColor ... +func NoColor(a ...interface{}) string { + return fmt.Sprint(a...) +} + +// Black ... +func Black(a ...interface{}) string { + return addColor(blackColor, fmt.Sprint(a...)) +} + +// Red ... +func Red(a ...interface{}) string { + return addColor(redColor, fmt.Sprint(a...)) +} + +// Green ... +func Green(a ...interface{}) string { + return addColor(greenColor, fmt.Sprint(a...)) +} + +// Yellow ... +func Yellow(a ...interface{}) string { + return addColor(yellowColor, fmt.Sprint(a...)) +} + +// Blue ... +func Blue(a ...interface{}) string { + return addColor(blueColor, fmt.Sprint(a...)) +} + +// Magenta ... +func Magenta(a ...interface{}) string { + return addColor(magentaColor, fmt.Sprint(a...)) +} + +// Cyan ... +func Cyan(a ...interface{}) string { + return addColor(cyanColor, fmt.Sprint(a...)) +} + +// ColorfFunc ... +type ColorfFunc func(format string, a ...interface{}) string + +// NoColorf ... +func NoColorf(format string, a ...interface{}) string { + return NoColor(fmt.Sprintf(format, a...)) +} + +// Blackf ... +func Blackf(format string, a ...interface{}) string { + return Black(fmt.Sprintf(format, a...)) +} + +// Redf ... +func Redf(format string, a ...interface{}) string { + return Red(fmt.Sprintf(format, a...)) +} + +// Greenf ... +func Greenf(format string, a ...interface{}) string { + return Green(fmt.Sprintf(format, a...)) +} + +// Yellowf ... +func Yellowf(format string, a ...interface{}) string { + return Yellow(fmt.Sprintf(format, a...)) +} + +// Bluef ... +func Bluef(format string, a ...interface{}) string { + return Blue(fmt.Sprintf(format, a...)) +} + +// Magentaf ... +func Magentaf(format string, a ...interface{}) string { + return Magenta(fmt.Sprintf(format, a...)) +} + +// Cyanf ... +func Cyanf(format string, a ...interface{}) string { + return Cyan(fmt.Sprintf(format, a...)) +} diff --git a/vendor/github.com/gorilla/mux/README.md b/vendor/github.com/gorilla/mux/README.md index 85953973..8dcd7188 100644 --- a/vendor/github.com/gorilla/mux/README.md +++ b/vendor/github.com/gorilla/mux/README.md @@ -274,7 +274,7 @@ This also works for host and query value variables: r := mux.NewRouter() r.Host("{subdomain}.domain.com"). Path("/articles/{category}/{id:[0-9]+}"). - Queries("filter", "{filter}") + Queries("filter", "{filter}"). HandlerFunc(ArticleHandler). Name("article") diff --git a/version/version.go b/version/version.go index ce8418bc..1609b877 100644 --- a/version/version.go +++ b/version/version.go @@ -1,4 +1,4 @@ package version // VERSION ... -const VERSION = "1.1.28" +const VERSION = "1.1.29"