Functioning In The Cloud
Welcome back! We are just done with our GitHub TODO issue creator thing. This time around we’re going to go through the steps to deploy it as a Google Cloud Function. To do this we’ll need to alter the code. But before we dive into the code itself we’ll need to get set up to use Go modules.
Go Mod
First, we’ll need to get our mod
file ready. You may or may not already have modules enabled. If you do you should be able to leave off the GO111MODULE=on
part of the command.
GO111MODULE=on go mod issuer
Now, we need to make sure we’ve got all the modules we need.
➜ issuer GO111MODULE=on go mod tidy
go: finding github.com/google/go-github/v25/github latest
go: finding golang.org/x/oauth2 latest
go: downloading google.golang.org/appengine v1.4.0
go: extracting google.golang.org/appengine v1.4.0
go: downloading github.com/golang/protobuf v1.2.0
go: extracting github.com/golang/protobuf v1.2.0
go: downloading golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6
go: extracting golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6
Looking good so far! Next, we want to vendor our dependencies. This copies the modules we downloaded in the previous step into our code directory. You don't really need to do this step, I'm including it anyway.
GO111MODULE=on go mod vendor
We should now see the vendor directory.
➜ issuer ls
go.mod go.sum main.go vendor
Alterations
Now let’s make a few changes to our code to get it ready for deploying as a Cloud Function. We’ll be starting off the same as last time. You may notice that I’ve removed the ability to query /status
, it is no longer needed.
package issuer
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strconv"
"github.com/google/go-github/v25/github"
"github.com/shindakun/envy"
"golang.org/x/oauth2"
)
Note that I’ve pulled out our constants and replaced them will variables. This allows us to make the code a bit more generic and so others can use it. We’ll cover how we are setting up these variables a bit later on. Note, some people may not like the use of global variables here. Since our function is so small and we have a good idea what’s happening in each step it’s OK to use them for now.
Of course, if you’d rather not have globals feel free to remove them. You can hard code as constants or load them inside our main function call.
// RepoOwner is the owner of the repo we want to open an issue in
var RepoOwner string
// IssueRepo is the repo we want to open this new issue in
var IssueRepo string
// ProjectColumn is the TODO column number of the project we want to add the issue to
var ProjectColumn int64
// Token is the GitHub Personal Access Token
var Token string
// Secret is used to validate payloads
var Secret string
// Payload of GitHub webhook
type Payload struct {
Action string `json:"action"`
Issue struct {
URL string `json:"url"`
RepositoryURL string `json:"repository_url"`
Number int `json:"number"`
Title string `json:"title"`
Body string `json:"body"`
} `json:"issue"`
Repository struct {
Name string `json:"name"`
} `json:"repository"`
}
I’ve also changed handleWebhook()
to HandleWebhook()
. Exporting the function is what allows us to call it as the cloud function.
func HandleWebhook(res http.ResponseWriter, req *http.Request) {
var Payload Payload
defer req.Body.Close()
p, err := github.ValidatePayload(req, []byte(Secret))
if err != nil {
http.Error(res, "bad request: "+err.Error(), 400)
log.Printf("bad request: %v", err.Error())
return
}
As @kunde21 pointed out last week we are better off using json.Unmarshal()
here. This also allows us to remove the imports for bytes
and io/ioutil
.
err = json.Unmarshal(p, &Payload)
if err != nil {
http.Error(res, "bad request: "+err.Error(), 400)
log.Printf("bad request: %v", err.Error())
return
}
err = createNewIssue(&Payload)
if err != nil {
log.Printf("bad request: %v", err.Error())
return
}
}
func createNewIssue(p *Payload) error {
log.Printf("Creating New Issue.\n")
log.Printf(" Name: %#v\n", p.Repository.Name)
log.Printf(" Title: %#v\n", p.Issue.Title)
log.Printf(" Body: %#v\n", p.Issue.Body)
log.Printf(" URL: %#v\n", p.Issue.URL)
ctx := context.Background()
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: Token},
)
tc := oauth2.NewClient(ctx, ts)
client := github.NewClient(tc)
title := fmt.Sprintf("[%s] %s", p.Repository.Name, p.Issue.Title)
body := fmt.Sprintf("%s\n%s/%s#%d", p.Issue.Body, RepoOwner, p.Repository.Name, p.Issue.Number)
issue := &github.IssueRequest{
Title: &title,
Body: &body,
}
ish, _, err := client.Issues.Create(ctx, RepoOwner, IssueRepo, issue)
if err != nil {
log.Printf("error: %v", err)
return err
}
id := *ish.ID
card := &github.ProjectCardOptions{
ContentID: id,
ContentType: "Issue",
}
_, _, err = client.Projects.CreateProjectCard(ctx, ProjectColumn, card)
if err != nil {
log.Printf("error: %v", err)
return err
}
return nil
}
Now we get to one of the biggest changes. Since the cloud function is going to call HandleWebhook()
, we no longer need our main()
. But that presents an issue, we have some environment variables we want to use. We could load them in the HandleWebhook()
call it’s more appropriate to make use of Go’s init()
.
init()
runs before main (or our handler in this case), which allows us to load our variables as normal.
func init() {
log.Println("Issuer")
var err error
Token, err = envy.Get("GITHUBTOKEN")
if err != nil || Token == "" {
log.Printf("error: %v", err)
os.Exit(1)
}
Secret, err = envy.Get("SECRET")
if err != nil || Secret == "" {
log.Printf("error: %v", err)
os.Exit(1)
}
RepoOwner, err = envy.Get("REPOOWNER")
if err != nil || RepoOwner == "" {
log.Printf("error: %v", err)
os.Exit(1)
}
IssueRepo, err = envy.Get("ISSUEREPO")
if err != nil || IssueRepo == "" {
log.Printf("error: %v", err)
os.Exit(1)
}
ProjectColumn
requires a bit more setting up since I never extended envy
to return int
s. So we need to convert to an int64
before we can use the column numeric ID to create our TODO card on the kanban board.
n, err := envy.Get("PROJECTCOLUMN")
if err != nil || n == "" {
log.Printf("error: %v", err)
os.Exit(1)
}
ProjectColumn, err = strconv.ParseInt(n, 10, 64)
if err != nil || ProjectColumn == 0 {
log.Printf("error: %v", err)
os.Exit(1)
}
}
Go To The Cloud
I am going to assume that you have the Google Cloud command line tools installed and a project set up. If you do not Google has some very good tutorials. Check out https://cloud.google.com/functions/docs/quickstart for more
Note: You wouldn’t want to have the secret and the token deployed like this in production. You would instead want to use Cloud KMS or Runtime Configurator. I’m living dangerously.
➜ issuer gcloud functions deploy issuer --entry-point HandleWebhook --runtime go111 --trigger-http --memory=128MB --set-env-vars SECRET=secret,GITHUBTOKEN=token,REPOOWNER=shindakun,ISSUEREPO=to,PROJECTCOLUMN=5647145
Deploying function (may take a while - up to 2 minutes)...done.
availableMemoryMb: 128
entryPoint: HandleWebhook
environmentVariables:
GITHUBTOKEN: token
ISSUEREPO: to
PROJECTCOLUMN: '5647145'
REPOOWNER: shindakun
SECRET: secret
httpsTrigger:
url: https://us-east1-golang-slackbot.cloudfunctions.net/issuer
labels:
deployment-tool: cli-gcloud
name: projects/golang-slackbot/locations/us-east1/functions/issuer
runtime: go111
serviceAccountEmail: golang-slackbot@appspot.gserviceaccount.com
sourceUploadUrl: https://storage.googleapis.com/gcf-upload-us-central1-eeb5af0e-fd09-4fe7-30851592ebba/bcc11c6f-55fc-4d73-864a-6a89813206a6.zip?GoogleAccessId=service-84452958925@gcf-gserviceaccount.com&Expires=1561244032&Signature=QX%2BKy5j6YTA6%D%3D
status: ACTIVE
timeout: 60s
updateTime: '2019-06-22T22:24:37Z'
versionId: '1'
Next time
And there we have it! We’re now live and update any source repos to use our trigger URL. That makes this stage of our issuer complete. New issues will appear in our target repo and on the TODO section of the kanban board!
What should we tackle next time? I’ll have to take a look at the kanban board and see if any ideas jump out at me. Until then feel free to let me know if you spot something to refactor.
You can find the code for this and most of the other Attempting to Learn Go posts in the repo on GitHub.
shindakun / atlg
Source repo for the "Attempting to Learn Go" posts I've been putting up over on dev.to
Attempting to Learn Go
Here you can find the code I've been writing for my Attempting to Learn Go posts that I've been writing and posting over on Dev.to.
Post Index
Enjoy this post? |
---|
How about buying me a coffee? |
Top comments (0)