Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions pkg/sources/github/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
Knative GitHub Source
=====================

This source will enable receiving of GitHub events from within Knative Eventing.

## Events

- dev.knative.github.pullrequest

## Images

Uses two images, the feedlet and the receive adapter.

### Feedlet

A Feedlet is a container to create or destroy event source bindings.

The Feedlet is run as a Job by the Feed controller.

This Feedlet will create the Receive Adapter and register a webhook in GitHub
for the account provided.

### Receive Adapter

The Receive Adapter is a Service.serving.knative.dev resource that is registered
to receive GitHub webhook requests. The source will wrap this request in a
CloudEvent and send it to the action provided.

## Usage

The GitHub Source expects there to be a secret in the following form:

```yaml
apiVersion: v1
kind: Secret
metadata:
name: githubsecret
type: Opaque
stringData:
githubCredentials: >
{
"accessToken": "<YOUR PERSONAL TOKEN FROM GITHUB>",
"secretToken": "<YOUR RANDOM STRING>"
}

```

The Feedlet requires a ServiceAccount to run as a cluster admin for the targeted namespace:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unfortunate that we can't put this in a yaml file somewhere to happen at the same time as the controller is installed/the source is created.


```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: feed-sa
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: feed-admin
subjects:
- kind: ServiceAccount
name: feed-sa
namespace: default
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io

```

To create a Receive Adapter (via the Feedlet), a Flow needs to exist:

```yaml
apiVersion: flows.knative.dev/v1alpha1
kind: Flow
metadata:
name: my-github-flow
namespace: default
spec:
serviceAccountName: feed-sa
trigger:
eventType: pullrequest
resource: <org>/<repo> # TODO: Fill this out
service: github
parameters:
secretName: githubsecret
secretKey: githubCredentials
parametersFrom:
- secretKeyRef:
name: githubsecret
key: githubCredentials
action:
target:
kind: Service
apiVersion: serving.knative.dev/v1alpha1
name: my-service

```
32 changes: 32 additions & 0 deletions pkg/sources/github/events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
Copyright 2018 The Knative Authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package github

const (
// Event Names
PullrequestEvent = "dev.knative.github.pullrequest"
UnsupportedEvent = "dev.knative.github.unsupported"
)

var CloudEventType = map[string]string{
"pull_request": PullrequestEvent,
"": UnsupportedEvent,
}

var GithubEventType = map[string]string{
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this just the reverse of CloudEventType?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, is there a better way?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build it automatically, like:

var GithubEventType map[string]string
for k, v := range(CloudEventType) {
  GitHubEventType[v] = k
}

PullrequestEvent: "pull_request",
}
3 changes: 1 addition & 2 deletions pkg/sources/github/eventsource.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ metadata:
name: github
namespace: default
spec:
type: github
source: github
image: github.com/knative/eventing/pkg/sources/github
image: github.com/knative/eventing/pkg/sources/github/feedlet
parameters:
image: github.com/knative/eventing/pkg/sources/github/receive_adapter
2 changes: 1 addition & 1 deletion pkg/sources/github/eventtype.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
apiVersion: feeds.knative.dev/v1alpha1
kind: EventType
metadata:
name: pullrequest
name: dev.knative.github.pullrequest
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-registration would make it easier to make this name align with the one in events.go

namespace: default
spec:
eventSource: github
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"os"

ghclient "github.com/google/go-github/github"
"github.com/knative/eventing/pkg/sources/github"
"github.com/knative/eventing/pkg/sources/github/resources"
"github.com/knative/serving/pkg/apis/serving/v1alpha1"
"k8s.io/apimachinery/pkg/watch"
Expand Down Expand Up @@ -451,11 +452,9 @@ func parseEventsFrom(eventType string) ([]string, error) {
if len(eventType) == 0 {
return []string(nil), fmt.Errorf("event type is empty")
}
switch eventType {
case "pullrequest":
return []string{"pull_request"}, nil
// TODO: Add more supported event types.
default:
event, ok := github.GithubEventType[eventType]
if !ok {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may also be able to check if event == ""

return []string(nil), fmt.Errorf("event type is unknown: %s", eventType)
}
return []string{event}, nil
}
68 changes: 47 additions & 21 deletions pkg/sources/github/receive_adapter/receive_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,18 @@ import (
"fmt"
"os"

webhooks "gopkg.in/go-playground/webhooks.v3"
"gopkg.in/go-playground/webhooks.v3/github"
"gopkg.in/go-playground/webhooks.v3"
gh "gopkg.in/go-playground/webhooks.v3/github"

"bytes"
"io/ioutil"
"net/http"

ghclient "github.com/google/go-github/github"
"github.com/google/uuid"
"github.com/knative/eventing/pkg/event"
"github.com/knative/eventing/pkg/sources/github"
"log"
"time"
)

const (
Expand All @@ -54,8 +57,26 @@ type GithubSecrets struct {
// HandlePullRequest is invoked whenever a PullRequest is modified (created, updated, etc.)
func (h *GithubHandler) HandlePullRequest(payload interface{}, header webhooks.Header) {
log.Print("Handling Pull Request")
pl := payload.(github.PullRequestPayload)
postMessage(h.target, &pl)

hdr := http.Header(header)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a webhooks.Header is an http.Header, so it should already have .Get().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not have access to .Get() from the webhooks.Header object for some reason, it was a compile error when I was trying.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha -- http.Header is an http.Header, which is a map[string[]string. But since it has a different type name, methods which take an http.Header as a receiver don't apply to webhooks.Header.

So glad they "minimize imports": https://github.com/go-playground/webhooks/blob/v3.13.0/webhooks.go#L8

It looks like this declaration was removed in v5.


pl := payload.(gh.PullRequestPayload)

source := pl.PullRequest.HTMLURL

eventType, ok := github.CloudEventType[hdr.Get("X-GitHub-Event")]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

github.CloudEventType should map "" to UnsupportedEvent

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does, this catches all other github events that have a real name but are not "" and "pull_request"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, right. I was thinking about the "header missing" case.

if !ok {
eventType = github.UnsupportedEvent
}

eventID := hdr.Get("X-GitHub-Delivery")
if len(eventID) == 0 {
if uuid, err := uuid.NewRandom(); err != nil {
eventID = uuid.String()
}
}

postMessage(h.target, payload, source, eventType, eventID)
}

func main() {
Expand Down Expand Up @@ -86,10 +107,10 @@ func main() {
target: target,
}

hook := github.New(&github.Config{Secret: credentials.SecretToken})
hook := gh.New(&gh.Config{Secret: credentials.SecretToken})
// TODO: GitHub has more than just Pull Request Events. This needs to
// handle them all?
hook.RegisterEvents(h.HandlePullRequest, github.PullRequestEvent)
hook.RegisterEvents(h.HandlePullRequest, gh.PullRequestEvent)

// TODO(n3wscott): Do we need to configure the PORT?
err = webhooks.Run(hook, ":8080", "/")
Expand All @@ -98,30 +119,35 @@ func main() {
}
}

func postMessage(target string, m *github.PullRequestPayload) error {
jsonStr, err := json.Marshal(m)
if err != nil {
log.Printf("Error: Failed to marshal the message: %+v : %v", m, err)
return err
}

func postMessage(target string, payload interface{}, source, eventType, eventID string) error {
URL := fmt.Sprintf("http://%s/", target)
req, err := http.NewRequest("POST", URL, bytes.NewBuffer(jsonStr))

ctx := event.EventContext{
CloudEventsVersion: event.CloudEventsVersion,
EventType: eventType,
EventID: eventID,
EventTime: time.Now(),
Source: source,
}
req, err := event.Binary.NewRequest(URL, payload, ctx)
if err != nil {
log.Printf("Error: Failed to create http request: %v", err)
log.Printf("Failed to marshal the message: %+v : %s", payload, err)
return err
}

req.Header.Set("Content-Type", "application/json")
log.Printf("Posting to %q", URL)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Printf("Error: Failed to do POST: %v", err)
return err
}
defer resp.Body.Close()
log.Printf("Error: response Status: %s", resp.Status)
body, _ := ioutil.ReadAll(resp.Body)
log.Printf("Error: response Body: %s", string(body))

if resp.StatusCode != http.StatusOK {
// TODO: in general, receive adapters may have to be able to retry for error cases.
log.Printf("response Status: %s", resp.Status)
body, _ := ioutil.ReadAll(resp.Body)
log.Printf("response Body: %s", string(body))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is a 500, should we return an error?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/idk

There are some questions here that are bigger than this PR around what the Receive Adapter should do if it gets an error from the "action". Should it try again? How many times?...

}
return nil
}
22 changes: 5 additions & 17 deletions sample/github/legit.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ import (
"flag"
"fmt"
ghclient "github.com/google/go-github/github"
"github.com/knative/eventing/pkg/event"
"golang.org/x/oauth2"
"gopkg.in/go-playground/webhooks.v3/github"
"io/ioutil"
"log"
"net/http"
"os"
Expand All @@ -49,20 +49,8 @@ type GithubSecrets struct {
SecretToken string `json:"secretToken"`
}

func (h *GithubHandler) handlePost(rw http.ResponseWriter, req *http.Request) {
body, err := ioutil.ReadAll(req.Body)
if err != nil {
panic(err)
}
rw.WriteHeader(200)

var pl github.PullRequestPayload
if err := json.Unmarshal(body, &pl); err != nil {
log.Printf("error: could not unmarshal payload %v", err)
return
}
func (h *GithubHandler) newPullRequestPayload(ctx context.Context, pl *github.PullRequestPayload) {

// Do whatever you want from here...
title := pl.PullRequest.Title
log.Printf("GOT PR with Title: %q", title)

Expand All @@ -76,7 +64,8 @@ func (h *GithubHandler) handlePost(rw http.ResponseWriter, req *http.Request) {
updatedPR := ghclient.PullRequest{
Title: &newTitle,
}
newPR, response, err := h.client.PullRequests.Edit(h.ctx, pl.Repository.Owner.Login, pl.Repository.Name, int(pl.Number), &updatedPR)
newPR, response, err := h.client.PullRequests.Edit(h.ctx,
pl.Repository.Owner.Login, pl.Repository.Name, int(pl.Number), &updatedPR)
if err != nil {
log.Printf("Failed to update PR: %s\n%s", err, response)
return
Expand Down Expand Up @@ -115,6 +104,5 @@ func main() {
ctx: ctx,
}

http.HandleFunc("/", h.handlePost)
log.Fatal(http.ListenAndServe(":8080", nil))
log.Fatal(http.ListenAndServe(":8080", event.Handler(h.newPullRequestPayload)))
}