Skip to content

Commit 5462a7d

Browse files
techknowlogickalexellis
authored andcommitted
Gitea automation using OpenFaaS blog post
Signed-off-by: Matti R <[email protected]>
1 parent 06a93bd commit 5462a7d

File tree

5 files changed

+293
-0
lines changed

5 files changed

+293
-0
lines changed

_posts/2021-01-31-gitea-faas.md

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
---
2+
title: "Gitea automation using OpenFaaS"
3+
description: "Switching to Gitea doesn’t mean having to give up your bot automations"
4+
date: 2021-01-31
5+
image: /images/2021-01-gitea/gitea-sticker-header.jpg
6+
categories:
7+
- gitea
8+
- severless
9+
- webhooks
10+
- automation
11+
author_staff_member: matti
12+
dark_background: true
13+
14+
---
15+
16+
Switching to Gitea doesn’t mean having to give up your bot automations.
17+
18+
## Introduction
19+
20+
Just like with GitHub, Gitea sends webhooks for actions taking place on your install. By connecting these actions with an OpenFaaS function, you can reduce the amount of manual work done to manage your opensource project and spend more time building.
21+
22+
In this post we will walk through building a simple bot to label pull-requests.
23+
24+
## Pre-requisites
25+
26+
For the purposes of this guide we'll use a local install of Kubernetes, but any Kubernetes cluster could be used. The tutorial should take you less than 15-30 minutes to try.
27+
28+
A quick way to install all the tools you need is to use the [arkade](https://get-arkade.dev) cli tool.
29+
30+
```bash
31+
# Get arkade, and move it to $PATH
32+
curl -sLS https://dl.get-arkade.dev | sh
33+
sudo mv arkade /usr/local/bin/
34+
35+
# Run Kubernetes locally
36+
arkade get kind
37+
38+
# OpenFaaS CLI
39+
arkade get faas-cli
40+
41+
# Create a cluster
42+
kind create cluster
43+
44+
# Install OpenFaaS
45+
arkade install openfaas
46+
47+
# Install Gitea
48+
arkade install gitea
49+
```
50+
51+
Next, you'll need to portforward both Gitea, and OpenFaaS, as well as login into the OpenFaaS gateway with faas-cli
52+
53+
```bash
54+
# Forward the OpenFaaS gateway to your machine
55+
kubectl rollout status -n openfaas deploy/gateway
56+
kubectl port-forward -n openfaas svc/gateway 8080:8080 &
57+
58+
# If basic auth is enabled, you can now log into your gateway:
59+
PASSWORD=$(kubectl get secret -n openfaas basic-auth -o jsonpath="{.data.basic-auth-password}" | base64 --decode; echo)
60+
echo -n $PASSWORD | faas-cli login --username admin --password-stdin
61+
62+
# Forward the Gitea application to your machine
63+
kubectl -n default port-forward svc/gitea-http 3000:3000 &
64+
```
65+
66+
## Build and deploy the Gitea bot function
67+
68+
For this guide we will be using golang for the Gitea bot, but you can use any language that you are comfortable working with.
69+
70+
To get started, we'll need to pull the prebuilt OpenFaaS template, and create the skeleton of a function using `faas-cli`.
71+
72+
```bash
73+
# Set to your Docker Hub account or registry address
74+
export OPENFAAS_PREFIX=techknowlogick
75+
76+
faas-cli template store pull golang-http
77+
faas-cli new lgtmbot --lang golang-middleware --prefix $OPENFAAS_PREFIX
78+
```
79+
80+
Now that the skeleton has been created, we'll need to start writing the function. The first thing to do is to write the webhook validation code.
81+
82+
```golang
83+
package function
84+
85+
import (
86+
"io/ioutil"
87+
"net/http"
88+
89+
scm "github.com/jenkins-x/go-scm/scm"
90+
giteaWebhook "github.com/jenkins-x/go-scm/scm/driver/gitea"
91+
)
92+
93+
func Handle(w http.ResponseWriter, r *http.Request) {
94+
webhookService := giteaWebhook.NewWebHookService()
95+
payload, err := webhookService.Parse(r, getWebhookSecret)
96+
if err != nil {
97+
// webhook failed to parse, either due to invalid secret or other reason
98+
w.WriteHeader(http.StatusBadRequest)
99+
return
100+
}
101+
102+
// ...
103+
}
104+
105+
func getWebhookSecret(scm.Webhook) (string, error) {
106+
secret, err := getAPISecret("webhook-secret")
107+
return string(secret), err
108+
}
109+
110+
func getAPISecret(secretName string) (secretBytes []byte, err error) {
111+
// read from the openfaas secrets folder
112+
return ioutil.ReadFile("/var/openfaas/secrets/" + secretName)
113+
}
114+
```
115+
116+
What happens in the above snippet is `getAPISecret` reads the secret which will be defined by faas-cli shortly, is transformed by `getWebhookSecret` into the function signature required by `Parse`, and `Parse` validates that the webhook signature matches the secret, and returns a parsed struct.
117+
118+
Next, we will take that parsed information and determine which PR is being worked on, and update the labels as needed.
119+
120+
```golang
121+
import (
122+
"strings"
123+
// ...
124+
"code.gitea.io/sdk/gitea"
125+
)
126+
127+
func Handle(w http.ResponseWriter, r *http.Request) {
128+
// ...
129+
130+
owner := ""
131+
repo := ""
132+
index := int64(0)
133+
// validate that we received a PR Hook
134+
switch v := payload.(type) {
135+
case *scm.PullRequestHook:
136+
owner = v.Repo.Namespace
137+
repo = v.Repo.Name
138+
index = int64(v.PullRequest.Number)
139+
default:
140+
// unexpected hook passed
141+
w.WriteHeader(http.StatusBadRequest)
142+
return
143+
}
144+
if index == 0 {
145+
// unexpected hook passed, PR should have an index
146+
w.WriteHeader(http.StatusBadRequest)
147+
return
148+
}
149+
150+
// get gitea secrets & setup client
151+
giteaHost, err := getAPISecret("gitea-host")
152+
if err != nil {
153+
// failed to get secret
154+
w.WriteHeader(http.StatusInternalServerError)
155+
return
156+
}
157+
giteaToken, err := getAPISecret("gitea-token")
158+
if err != nil {
159+
// failed to get secret
160+
w.WriteHeader(http.StatusInternalServerError)
161+
}
162+
giteaClient, err := gitea.NewClient(string(giteaHost), gitea.SetToken(string(giteaToken)))
163+
if err != nil {
164+
// failed to setup gitea client
165+
w.WriteHeader(http.StatusInternalServerError)
166+
}
167+
168+
// fetch PR and approvals
169+
pr, _, err := giteaClient.GetPullRequest(owner, repo, index)
170+
if err != nil {
171+
// failed to fetch PR
172+
w.WriteHeader(http.StatusInternalServerError)
173+
return
174+
}
175+
approvals, _, err := giteaClient.ListPullReviews(owner, repo, index, gitea.ListPullReviewsOptions{})
176+
if err != nil {
177+
// failed to fetch approvals
178+
w.WriteHeader(http.StatusInternalServerError)
179+
return
180+
}
181+
182+
// determine which LGTM label should be used
183+
approvalCount := 0
184+
for _, approval := range approvals {
185+
if approval.State == gitea.ReviewStateApproved {
186+
approvalCount++
187+
}
188+
}
189+
labelNeeded := "lgtm/done"
190+
switch approvalCount {
191+
case 0:
192+
labelNeeded = "lgtm/need 2"
193+
case 1:
194+
labelNeeded = "lgtm/need 1"
195+
}
196+
197+
// loop thourgh existing labels to determine if an update is needed
198+
needUpdate := true
199+
for _, label := range pr.Labels {
200+
if !strings.HasPrefix(label.Name, "lgtm/") {
201+
continue
202+
}
203+
if label.Name == labelNeeded {
204+
needUpdate = false
205+
continue
206+
}
207+
// if label starts with "lgtm/" but isn't the correct label
208+
giteaClient.DeleteIssueLabel(owner, repo, index, label.ID)
209+
}
210+
if !needUpdate {
211+
// no label changes required
212+
w.WriteHeader(http.StatusOK)
213+
return
214+
}
215+
216+
// if needed label not set, then set it
217+
// fetch ID of labelNeeded
218+
giteaLabels, _, err := giteaClient.ListRepoLabels(owner, repo, gitea.ListLabelsOptions{})
219+
if err != nil {
220+
// failed to fetch labels
221+
w.WriteHeader(http.StatusInternalServerError)
222+
return
223+
}
224+
labelID := int64(0)
225+
for _, label := range giteaLabels {
226+
if label.Name == labelNeeded {
227+
labelID = label.ID
228+
}
229+
}
230+
if labelID == 0 {
231+
// failed to find label, TODO: create label
232+
w.WriteHeader(http.StatusInternalServerError)
233+
return
234+
}
235+
// set label on PR
236+
createSlice := []int64{int64(labelID)}
237+
_, _, err = giteaClient.AddIssueLabels(owner, repo, index, gitea.IssueLabelsOption{createSlice})
238+
if err != nil {
239+
// failed to set label
240+
w.WriteHeader(http.StatusInternalServerError)
241+
return
242+
}
243+
244+
// all fine
245+
w.WriteHeader(http.StatusOK)
246+
return
247+
}
248+
249+
// ...
250+
```
251+
252+
With the function now built, now it is time to deploy the function. First you'll need to setup the secrets, by defining them in the function yaml, and setting them via `faas-cli`
253+
254+
```yaml
255+
build_args:
256+
GO111MODULE: on
257+
secrets:
258+
- webhook-secret # random string generated by you
259+
- gitea-host # host of your gitea instance, ex. https://gitea.example.com/
260+
- gitea-token # gitea api token generated from https://gitea.example.com/user/settings/applications
261+
```
262+
263+
```bash
264+
faas-cli secret create webhook-secret --from-literal "abc123"
265+
faas-cli secret create gitea-host --from-literal "http://gitea-http.default.svc.cluster.local:3000/"
266+
faas-cli secret create gitea-token --from-literal "GET FROM GITEA GUI"
267+
268+
faas-cli up -f lgtmbot.yml
269+
270+
```
271+
272+
Finally, we need to configure the repo in Gitea that you would like to manage with this function. This can be done by going to the webhooks settings of the repo (ex https://gitea.example.com/org/repo/settings/hooks), and creating a new Gitea webhook with the secret being the one you set above.
273+
274+
![setup webhook in gitea](/images/2021-01-gitea/setup_gitea_webhook.png)
275+
276+
## Wrapping Up
277+
278+
Now that we’ve seen how to create a simple bot using OpenFaaS, from here we can build upon this base and make more complex actions. The next step could be transforming the [lockbot](https://www.openfaas.com/blog/schedule-your-functions/) from an earlier post to support Gitea as well, or even extending [Derek](https://github.com/alexellis/derek/) to support Gitea. The possibilities are endless. Eventually we could end up with a number of functions to rival the GitHub apps marketplace.
279+
280+
This post uses OpenFaaS, but you could use faasd to keep the installation requirements a minimum.
281+
282+
### Taking it further
283+
284+
For a production ready OpenFaaS function that supports automation in Gitea you can view the [Gitea/Buildkite connector](https://github.com/techknowlogick/gitea-buildkite-connector).

_staff_members/matti.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
name: Matti Ranta
3+
position: Contributor
4+
image_path: /images/author/matti.png
5+
twitter_username: techknowlogick
6+
github_username: techknowlogick
7+
webpage: https://www.techknowlogick.com/
8+
blurb: Open source enthusiast, and one of <a href="https://gitea.io">Gitea's</a> project leads.
9+
---
119 KB
Loading
68.4 KB
Loading

images/author/matti.png

406 KB
Loading

0 commit comments

Comments
 (0)