待望のCloud FunctionsのGo対応! Cloud Buildの通知をGoで実装!!

## tl;dr

- Cloud Functions GoでCloud Buildの結果通知をSlack投稿するサンプルを書いた
- ソースはこちら
    - [go\-cloud\-functions\-examples/notify\-slack at master · iwata/go\-cloud\-functions\-examples](https://github.com/iwata/go-cloud-functions-examples/tree/master/notify-slack)

## Cloud Functions Go

つい先日(2019/1/17)、Cloud FunctionsでGoが使えるようになりました:tada: 
[Cloud Functions: Go 1\.11 is now a supported language \| Google Cloud Blog](https://cloud.google.com/blog/products/application-development/cloud-functions-go-1-11-is-now-a-supported-language)
いまのところベータ版ですが、GCPのベータはプロダクションレベルなんでいろいろ使っていきたいですね。

## Cloud Buildの通知サンプル

Cloud Buildの実行結果を、Cloud Functionsを使ってSlackに通知するサンプルがオフィシャルドキュメントにあります。
[Configuring notifications for third\-party services  \|  Cloud Build Documentation  \|  Google Cloud](https://cloud.google.com/cloud-build/docs/configure-third-party-notifications)
Node.jsで書かれたものですが、こちらを今回Goで書き直してみました。

## Go版のCloud Functions

ソースは以下です。[^1]

```go:function.go
package function

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"os"

	"cloud.google.com/go/functions/metadata"
	slack "github.com/ashwanthkumar/slack-go-webhook"
	"github.com/pkg/errors"
	"google.golang.org/api/cloudbuild/v1"
)

const SlackWebhookURL = "[SLACK_WEBHOOK]"

var (
	projectID string
	resource  string
	// Skip if the current status is not in the status list.
	// Add additional statues to list if you'd like:
	// QUEUED, WORKING, SUCCESS, FAILURE,
	// INTERNAL_ERROR, TIMEOUT, CANCELLED
	status = map[string]bool{
		"SUCCESS":        true,
		"FAILURE":        true,
		"INTERNAL_ERROR": true,
		"TIMEOUT":        true,
	}
)

func init() {
	projectID = os.Getenv("GCP_PROJECT")
	resource = fmt.Sprintf("projects/%s/topics/cloud-builds", projectID)
}

type PubSubMessage struct {
	Data string `json:"data"`
}

// Subscribe is the main function called by Cloud Functions.
func Subscribe(ctx context.Context, m PubSubMessage) error {
	meta, err := metadata.FromContext(ctx)
	if err != nil {
		return errors.Wrap(err, "Failed to get metadata")
	}
	if meta.Resource.Name != resource {
		fmt.Printf("%s is not wathing resource\n", meta.Resource.Name)
		return nil
	}

	build, err := eventToBuild(m.Data)
	if err != nil {
		return errors.Wrap(err, "Failed to decode event data")
	}

	if _, ok := status[build.Status]; !ok {
		fmt.Printf("%s status is skipped\n", build.Status)
		return nil
	}

	// Send message to Slack.
	message := createSlackMessage(build)
	errs := slack.Send(SlackWebhookURL, "", message)
	if len(errs) > 0 {
		return errors.Errorf("Failed to send a message to Slack: %s", errs)
	}

	return nil
}

// eventToBuild transforms pubsub event message to a Build struct.
func eventToBuild(data string) (*cloudbuild.Build, error) {
	d, err := base64.StdEncoding.DecodeString(data)
	if err != nil {
		return nil, errors.Wrap(err, "Failed to decode base64 data")
	}

	build := cloudbuild.Build{}
	err = json.Unmarshal(d, &build)
	if err != nil {
		return nil, errors.Wrap(err, "Failed to decode to JSON")
	}
	return &build, nil
}

// createSlackMessage creates a message from a build object.
func createSlackMessage(build *cloudbuild.Build) slack.Payload {
	title := "Build Logs"
	a := slack.Attachment{
		Title:     &title,
		TitleLink: &build.LogUrl,
	}
	a.AddField(slack.Field{
		Title: "Status",
		Value: build.Status,
	})
	p := slack.Payload{
		Text:        fmt.Sprintf("Build `%s`", build.Id),
		Markdown:    true,
		Attachments: []slack.Attachment{a},
	}
	return p
}
```

[^1]: ソースは[Github](https://github.com/iwata/go-cloud-functions-examples/tree/master/notify-slack)にもあげてあります。

追い易いように元のNode.jsのソースのコメントほぼそのまま載せました。
Node.js版をみるとわかるように、Cloud Buildから渡ってくるPubSub Messageは`data`フィールドをもっており、その中身はJSONをbase64エンコードしたものになっています。
ここからデータを取り出すために、Goでもbase64デコード→JSONデコードの処理を`eventToBuild`で実装しています。
ビルドデータの構造体はGoogleの`cloudbuild.v1`パッケージの[Build構造体](https://godoc.org/google.golang.org/api/cloudbuild/v1#Build)で定義されているものを使っています。

`status`をチェックしているところは、Nodeだと`Array.prototype.indexOf`を使っていますが、Goのスライスだとそういうメソッドはなく、プリミティブに`for`を書くことになるので、`map`のキーとして有効な`status`を定義しました。

SlackにInCommingWebhook経由で通知する部分は、[ashwanthkumar/slack\-go\-webhook](https://github.com/ashwanthkumar/slack-go-webhook)[^2]を使いました。
[^2]: パッと調べた限りこれ以外ライブラリなさそうだった

これで以下のようなコマンドでデプロイすれば使えるはず…です![^3]

```sh
$ gcloud functions deploy Subscribe --runtime go111 \
  --trigger-topic cloud-builds
```

[^3]: 実際にこのソースで試してはないですが、いまやっているプロジェクトでは似たようなコードで動いています。