|
| 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 | + |
| 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). |
0 commit comments