From f2232ab01a920cc8bc1cbce5f37aa2eb0dad5450 Mon Sep 17 00:00:00 2001
From: Andrea Frittoli
Date: Tue, 19 Nov 2019 05:16:11 -0800
Subject: [PATCH] Setup a mario bot to build images on demand
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
---
cmd/mario/kodata/LICENSE | 1 +
cmd/mario/kodata/OWNERS | 1 +
cmd/mario/main.go | 112 +++++++++++
config/100-namespace.yaml | 18 ++
config/200-serviceaccount.yaml | 19 ++
config/mario.yaml | 53 +++++
pipelinerun-logs/config/deployment.yaml | 4 +-
tekton/resources/mario-github-comment.yaml | 158 +++++++++++++++
.../resources/mario-image-build-trigger.yaml | 187 ++++++++++++++++++
9 files changed, 552 insertions(+), 1 deletion(-)
create mode 120000 cmd/mario/kodata/LICENSE
create mode 120000 cmd/mario/kodata/OWNERS
create mode 100644 cmd/mario/main.go
create mode 100644 config/100-namespace.yaml
create mode 100644 config/200-serviceaccount.yaml
create mode 100644 config/mario.yaml
create mode 100644 tekton/resources/mario-github-comment.yaml
create mode 100644 tekton/resources/mario-image-build-trigger.yaml
diff --git a/cmd/mario/kodata/LICENSE b/cmd/mario/kodata/LICENSE
new file mode 120000
index 0000000000..5853aaea53
--- /dev/null
+++ b/cmd/mario/kodata/LICENSE
@@ -0,0 +1 @@
+../../../LICENSE
\ No newline at end of file
diff --git a/cmd/mario/kodata/OWNERS b/cmd/mario/kodata/OWNERS
new file mode 120000
index 0000000000..0cca7e5248
--- /dev/null
+++ b/cmd/mario/kodata/OWNERS
@@ -0,0 +1 @@
+../../../OWNERS
\ No newline at end of file
diff --git a/cmd/mario/main.go b/cmd/mario/main.go
new file mode 100644
index 0000000000..b315a68aae
--- /dev/null
+++ b/cmd/mario/main.go
@@ -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))
+}
diff --git a/config/100-namespace.yaml b/config/100-namespace.yaml
new file mode 100644
index 0000000000..ddfa798328
--- /dev/null
+++ b/config/100-namespace.yaml
@@ -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
diff --git a/config/200-serviceaccount.yaml b/config/200-serviceaccount.yaml
new file mode 100644
index 0000000000..d0a939e1cf
--- /dev/null
+++ b/config/200-serviceaccount.yaml
@@ -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
diff --git a/config/mario.yaml b/config/mario.yaml
new file mode 100644
index 0000000000..51dff1c4aa
--- /dev/null
+++ b/config/mario.yaml
@@ -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
diff --git a/pipelinerun-logs/config/deployment.yaml b/pipelinerun-logs/config/deployment.yaml
index 61c9bab25d..56b7e84899 100644
--- a/pipelinerun-logs/config/deployment.yaml
+++ b/pipelinerun-logs/config/deployment.yaml
@@ -27,6 +27,8 @@ spec:
- "dogfooding"
- "--namespace"
- "default"
+ - "--namespace"
+ - "mario"
- "--hostname"
- "0.0.0.0"
- "--port"
@@ -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"
diff --git a/tekton/resources/mario-github-comment.yaml b/tekton/resources/mario-github-comment.yaml
new file mode 100644
index 0000000000..35bcbd79f1
--- /dev/null
+++ b/tekton/resources/mario-github-comment.yaml
@@ -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 = (
+ ''
+ ' at your service!
'
+ )
+
+ 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: '
+ 'built image|'
+ )
+ comment_params['imageurl'] = '$(outputs.resources.image.url)'
+ else:
+ comment_template += (
+ 'Cloud not build the requested image. Please check the '
+ )
+
+ comment_template += (
+ 'build logs'
+ )
+ 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)
diff --git a/tekton/resources/mario-image-build-trigger.yaml b/tekton/resources/mario-image-build-trigger.yaml
new file mode 100644
index 0000000000..60124b9e55
--- /dev/null
+++ b/tekton/resources/mario-image-build-trigger.yaml
@@ -0,0 +1,187 @@
+kind: Role
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+ name: triggers-minimal
+rules:
+- apiGroups: ["tekton.dev"]
+ resources: ["eventlisteners", "triggerbindings", "triggertemplates", "tasks", "taskruns"]
+ verbs: ["get"]
+- apiGroups: ["tekton.dev"]
+ resources: ["pipelineruns", "pipelineresources", "taskruns"]
+ verbs: ["create"]
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: mario-listener
+secrets:
+- name: mario-github-secret
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: mario-releaser
+secrets:
+- name: release-secret
+- name: mario-github-secret
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: mario-releaser-triggers-minimal
+subjects:
+- kind: ServiceAccount
+ name: mario-releaser
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: triggers-minimal
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: mario-listener-triggers-minimal
+subjects:
+- kind: ServiceAccount
+ name: mario-listener
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: triggers-minimal
+---
+apiVersion: tekton.dev/v1alpha1
+kind: TriggerBinding
+metadata:
+ name: trigger-to-build-and-push-image
+spec:
+ params:
+ - name: buildUUID
+ value: $(body.buildUUID)
+ - name: gitRepository
+ value: $(body.gitRepository)
+ - name: gitRevision
+ value: $(body.gitRevision)
+ - name: contextPath
+ value: $(body.contextPath)
+ - name: targetImage
+ value: $(body.targetImage)
+ - name: pullRequestID
+ value: $(body.pullRequestID)
+---
+apiVersion: tekton.dev/v1alpha1
+kind: EventListener
+metadata:
+ name: mario-image-builder
+spec:
+ serviceAccountName: mario-listener
+ serviceType: LoadBalancer
+ triggers:
+ - name: trigger
+ interceptor:
+ objectRef:
+ kind: Service
+ name: mario
+ apiVersion: v1
+ namespace: mario
+ binding:
+ name: trigger-to-build-and-push-image
+ template:
+ name: build-and-push-image
+---
+apiVersion: tekton.dev/v1alpha1
+kind: PipelineResource
+metadata:
+ name: github-feedback-trigger
+spec:
+ type: cloudEvent
+ params:
+ - name: targetURI
+ value: http://el-github-feedback-trigger.mario:8080
+---
+apiVersion: tekton.dev/v1alpha1
+kind: TriggerTemplate
+metadata:
+ name: build-and-push-image
+spec:
+ params:
+ - name: gitRepository
+ description: The git repository that hosts context and Dockerfile
+ - name: gitRevision
+ description: The Git revision to be used.
+ - name: contextPath
+ description: The path to the context within 'gitRepository'
+ - name: targetImage
+ description: The fully qualifie image target e.g. repo/name:tag.
+ resourcetemplates:
+ - apiVersion: tekton.dev/v1alpha1
+ kind: PipelineResource
+ metadata:
+ name: target-image-$(uid)
+ spec:
+ type: image
+ params:
+ - name: url
+ value: $(params.targetImage)
+ - apiVersion: tekton.dev/v1alpha1
+ kind: TaskRun
+ metadata:
+ generateName: build-and-push-$(uid)-
+ labels:
+ prow.k8s.io/build-id: $(params.buildUUID)
+ mario.bot/pull-request-id: $(params.pullRequestID)
+ spec:
+ serviceAccountName: mario-releaser
+ taskSpec:
+ inputs:
+ params:
+ - name: contextPath
+ description: The path to the context
+ resources:
+ - name: source
+ type: git
+ outputs:
+ resources:
+ - name: image
+ type: image
+ - name: endtrigger
+ type: cloudEvent
+ steps:
+ - name: build-and-push
+ workingdir: $(inputs.resources.source.path)
+ image: gcr.io/kaniko-project/executor:v0.13.0
+ env:
+ - name: GOOGLE_APPLICATION_CREDENTIALS
+ value: /secret/release.json
+ command:
+ - /kaniko/executor
+ - --dockerfile=Dockerfile
+ - --context=$(inputs.params.contextPath)
+ - --destination=$(params.targetImage)
+ volumeMounts:
+ - name: gcp-secret
+ mountPath: /secret
+ volumes:
+ - name: gcp-secret
+ secret:
+ secretName: release-secret
+ inputs:
+ params:
+ - name: contextPath
+ value: $(params.contextPath)
+ resources:
+ - name: source
+ resourceSpec:
+ type: git
+ params:
+ - name: revision
+ value: $(params.gitRevision)
+ - name: url
+ value: https://$(params.gitRepository)
+ outputs:
+ resources:
+ - name: image
+ resourceRef:
+ name: target-image-$(uid)
+ - name: endtrigger
+ resourceRef:
+ name: github-feedback-trigger