Skip to content

Commit

Permalink
Setup a mario bot to build images on demand
Browse files Browse the repository at this point in the history
Define an event-listener that can handle GitHub webhooks, using an
interceptor "mario" service.

The interceptor will:
- validate the event with the shared secret
- filter new comment events only
- filter comments that start with "/mario" only
- for now only accept the syntax "/mario build [contextPath]
- return a body with the minimal content for the trigger binding

A tekton tasks build the request images and triggers a cloud event.
The pull request URL is attached to the taskrun with a label,
that is sent via the cloud event.

Define an event listener that receives the cloud event, and triggers
something (TBD) that posts back to GitHub a comment with:
- a link to the logs app to see the build logs
- a link to the built image with the sha
- a random picture of mario from the talk
  • Loading branch information
afrittoli committed Nov 26, 2019
1 parent 51c53a3 commit f2232ab
Show file tree
Hide file tree
Showing 9 changed files with 552 additions and 1 deletion.
1 change: 1 addition & 0 deletions cmd/mario/kodata/LICENSE
1 change: 1 addition & 0 deletions cmd/mario/kodata/OWNERS
112 changes: 112 additions & 0 deletions cmd/mario/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
Copyright 2019 The Tekton 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 main

import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strconv"
"strings"

"github.com/google/go-github/github"
"github.com/google/uuid"
)

const (
// Environment variable containing GitHub secret token
envSecret = "GITHUB_SECRET_TOKEN"
)

type triggerPayload struct {
BuildUUID string `json:"buildUUID,omitempty"`
GitRepository string `json:"gitRepository,omitempty"`
GitRevision string `json:"gitRevision,omitempty"`
ContextPath string `json:"contextPath,omitempty"`
TargetImage string `json:"targetImage,omitempty"`
PullRequestID string `json:"pullRequestID,omitempty"`
}

func main() {
errorMessage := ""
secretToken := os.Getenv(envSecret)
if secretToken == "" {
log.Fatalf("No secret token given")
}

http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
//TODO: We should probably send over the EL eventID as a X-Tekton-Event-Id header as well
payload, err := github.ValidatePayload(request, []byte(secretToken))
id := github.DeliveryID(request)
if err != nil {
log.Printf("Error handling Github Event with delivery ID %s : %q", id, err)
http.Error(writer, fmt.Sprint(err), http.StatusBadRequest)
}
event, err := github.ParseWebHook(github.WebHookType(request), payload)
if err != nil {
log.Printf("Error handling Github Event with delivery ID %s : %q", id, err)
http.Error(writer, fmt.Sprint(err), http.StatusBadRequest)
}
switch event := event.(type) {
case *github.IssueCommentEvent:
if event.GetAction() == "created" {
eventBody := event.GetComment().GetBody()
if strings.HasPrefix(eventBody, "/mario") {
log.Printf("Handling Mario command with delivery ID: %s; Comment: %s", id, eventBody)
commandParts := strings.Fields(eventBody)
command := commandParts[1]
if command == "build" {
// No validation here. Anything beyond commandParts[3] is ignored
prID := strconv.Itoa(int(event.GetIssue().GetNumber()))
triggerBody := triggerPayload{
BuildUUID: uuid.New().String(),
GitRepository: "github.com/" + event.GetRepo().GetFullName(),
GitRevision: "pull/" + prID + "/head",
ContextPath: commandParts[2],
TargetImage: "us.icr.io/knative/" + commandParts[3],
PullRequestID: prID,
}
tPayload, err := json.Marshal(triggerBody)
if err != nil {
log.Printf("Failed to marshal the trigger body. Error: %q", err)
}
n, err := writer.Write(tPayload)
if err != nil {
log.Printf("Failed to write response for Github event ID: %s. Bytes writted: %d. Error: %q", id, n, err)
}
} else {
errorMessage = "Unknown Mario command"
}
} else {
errorMessage = "Not a Mario command"
}
} else {
errorMessage = "Only new comments are supported"
}
default:
errorMessage = "Event type not supported"
}
if errorMessage != "" {
log.Printf(errorMessage)
http.Error(writer, fmt.Sprint(errorMessage), http.StatusBadRequest)
}
})

log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", 8080), nil))
}
18 changes: 18 additions & 0 deletions config/100-namespace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright 2019 The Tekton 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.

apiVersion: v1
kind: Namespace
metadata:
name: mario
19 changes: 19 additions & 0 deletions config/200-serviceaccount.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright 2019 The Tekton 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.

apiVersion: v1
kind: ServiceAccount
metadata:
name: mario-bot
namespace: mario
53 changes: 53 additions & 0 deletions config/mario.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Copyright 2019 The Tekton 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
#
# https://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.

apiVersion: apps/v1
kind: Deployment
metadata:
name: mario
namespace: mario
spec:
replicas: 1
selector:
matchLabels:
app: mario
template:
metadata:
labels:
app: mario
spec:
serviceAccountName: mario-bot
containers:
- name: mario-interceptor
image: github.com/tektoncd/plumbing/cmd/mario
env:
- name: GITHUB_SECRET_TOKEN
valueFrom:
secretKeyRef:
name: mario-github-secret
key: secret-token
---
apiVersion: v1
kind: Service
metadata:
name: mario
namespace: mario
spec:
type: ClusterIP
selector:
app: mario
ports:
- protocol: TCP
port: 80
targetPort: 8080
4 changes: 3 additions & 1 deletion pipelinerun-logs/config/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ spec:
- "dogfooding"
- "--namespace"
- "default"
- "--namespace"
- "mario"
- "--hostname"
- "0.0.0.0"
- "--port"
Expand All @@ -35,4 +37,4 @@ spec:
- containerPort: 9999
nodeSelector:
# Schedule this deployment onto nodes with workload identity enabled
iam.gke.io/gke-metadata-server-enabled: true
iam.gke.io/gke-metadata-server-enabled: "true"
158 changes: 158 additions & 0 deletions tekton/resources/mario-github-comment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
apiVersion: tekton.dev/v1alpha1
kind: TriggerBinding
metadata:
name: trigger-to-comment-github
spec:
params:
- name: pullRequestID
value: $(body.taskRun.metadata.labels.mario\.bot/pull-request-id)
- name: buildUUID
value: $(body.taskRun.metadata.labels.prow\.k8s\.io/build-id)
- name: gitURL
value: $(body.taskRun.spec.inputs.resources.#(name=="source").resourceSpec.params.#(name=="url").value)
- name: gitRevision
value: $(body.taskRun.spec.inputs.resources.#(name=="source").resourceSpec.params.#(name=="revision").value)
- name: targetImageResourceName
value: $(body.taskRun.spec.outputs.resources.#(name=="image").resourceRef.name)
- name: passedOrFailed
value: $(body.taskRun.status.conditions.#(type=="Succeeded").status)
---
apiVersion: tekton.dev/v1alpha1
kind: EventListener
metadata:
name: github-feedback-trigger
spec:
serviceAccountName: mario-listener
triggers:
- name: trigger
binding:
name: trigger-to-comment-github
template:
name: mario-comment-github
---
apiVersion: tekton.dev/v1alpha1
kind: TriggerTemplate
metadata:
name: mario-comment-github
spec:
params:
- name: pullRequestID
description: The pullRequestID to comment to
- name: buildUUID
description: The buildUUID for the logs link
- name: gitURL
description: The URL of the git repo
- name: gitRevision
description: The git revision
- name: targetImageResourceName
description: The name of the target image pipelineresource
- name: passedOrFailed
description: Whether the triggering event was successful or not
resourcetemplates:
- apiVersion: tekton.dev/v1alpha1
kind: PipelineResource
metadata:
name: pr-$(uid)
spec:
type: pullRequest
params:
- name: url
value: $(params.gitURL)/pull/$(params.pullRequestID)
secrets:
- fieldName: githubToken
secretName: mario-github-token
secretKey: GITHUB_TOKEN
- apiVersion: tekton.dev/v1alpha1
kind: TaskRun
metadata:
generateName: mario-comment-github-$(uid)-
spec:
serviceAccountName: mario-listener
taskSpec:
inputs:
resources:
- name: source
type: git
- name: pr
type: pullRequest
outputs:
resources:
- name: image
type: image
- name: pr
type: pullRequest
steps:
- name: copy-pr-to-output
image: busybox
script: |
#!/bin/sh
mkdir -p $(outputs.resources.pr.path)
cp -r $(inputs.resources.pr.path)/* $(outputs.resources.pr.path)/
- name: setup-comment
image: python:3-alpine
script: |
#!/usr/bin/env python
import json
import random
marios_pics_root = 'https://storage.googleapis.com/mario-bot/pics'
ok_pics = ['mario', 'luigi', 'tekton']
failed_pics = ['goomba']
logs_url = 'http://35.222.249.224/?buildid=%s&namespace=mario'
successful = ($(params.passedOrFailed) == "True")
# Service Image
comment_template = (
'<img width="200" alt="{pic_alt}" src="{pic_src}">'
' at your service! </p>'
)
if successful:
chosen_pic = random.choice(ok_pics)
else:
chosen_pic = random.choice(failed_pics)
pic_url = "/".join([marios_pics_root, chosen_pic]) + '.png'
comment_params = dict(pic_alt=chosen_pic, pic_src=pic_url)
if successful:
comment_template += (
'Here is the image you requested: '
'<a href="https://{imageurl}">built image</a>|'
)
comment_params['imageurl'] = '$(outputs.resources.image.url)'
else:
comment_template += (
'Cloud not build the requested image. Please check the '
)
comment_template += (
'<a href="http://35.222.249.224/?buildid={buildid}&'
'namespace=mario">build logs</a>'
)
comment_params['buildid'] = '$(params.buildUUID)'
new_comment_path = "$(outputs.resources.pr.path)/comments/new.json"
comment_body = dict(body=comment_template.format(**comment_params))
with open(new_comment_path, "w") as comment:
json.dump(comment_body, comment)
inputs:
resources:
- name: source
resourceSpec:
type: git
params:
- name: revision
value: $(params.gitRevision)
- name: url
value: $(params.gitURL)
- name: pr
resourceRef:
name: pr-$(uid)
outputs:
resources:
- name: image
resourceRef:
name: $(params.targetImageResourceName)
- name: pr
resourceRef:
name: pr-$(uid)
Loading

0 comments on commit f2232ab

Please sign in to comment.