googlechat/main.go (196 lines of code) (raw):
// Copyright 2020 Google LLC
//
// 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 (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/GoogleCloudPlatform/cloud-build-notifiers/lib/notifiers"
log "github.com/golang/glog"
chat "google.golang.org/api/chat/v1"
cbpb "cloud.google.com/go/cloudbuild/apiv1/v2/cloudbuildpb"
)
const (
webhookURLSecretName = "webhookUrl"
)
func main() {
if err := notifiers.Main(new(googlechatNotifier)); err != nil {
log.Fatalf("fatal error: %v", err)
}
}
type googlechatNotifier struct {
filter notifiers.EventFilter
webhookURL string
}
func (g *googlechatNotifier) SetUp(ctx context.Context, cfg *notifiers.Config, _ string, sg notifiers.SecretGetter, _ notifiers.BindingResolver) error {
prd, err := notifiers.MakeCELPredicate(cfg.Spec.Notification.Filter)
if err != nil {
return fmt.Errorf("failed to make a CEL predicate: %w", err)
}
g.filter = prd
wuRef, err := notifiers.GetSecretRef(cfg.Spec.Notification.Delivery, webhookURLSecretName)
if err != nil {
return fmt.Errorf("failed to get Secret ref from delivery config (%v) field %q: %w", cfg.Spec.Notification.Delivery, webhookURLSecretName, err)
}
wuResource, err := notifiers.FindSecretResourceName(cfg.Spec.Secrets, wuRef)
if err != nil {
return fmt.Errorf("failed to find Secret for ref %q: %w", wuRef, err)
}
wu, err := sg.GetSecret(ctx, wuResource)
if err != nil {
return fmt.Errorf("failed to get token secret: %w", err)
}
g.webhookURL = wu
return nil
}
func (g *googlechatNotifier) SendNotification(ctx context.Context, build *cbpb.Build) error {
if !g.filter.Apply(ctx, build) {
return nil
}
log.Infof("sending Google Chat webhook for Build %q (status: %q)", build.Id, build.Status)
msg, err := g.writeMessage(build)
if err != nil {
return fmt.Errorf("failed to write Google Chat message: %w", err)
}
payload := new(bytes.Buffer)
err = json.NewEncoder(payload).Encode(msg)
if err != nil {
return fmt.Errorf("failed to encode payload: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, g.webhookURL, payload)
if err != nil {
return fmt.Errorf("failed to create a new HTTP request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "GCB-Notifier/0.1 (http)")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("failed to make HTTP request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Warningf("got a non-OK response status %q (%d) from %q", resp.Status, resp.StatusCode, g.webhookURL)
}
log.V(2).Infoln("send HTTP request successfully")
return nil
}
func (g *googlechatNotifier) writeMessage(build *cbpb.Build) (*chat.Message, error) {
var icon string
switch build.Status {
case cbpb.Build_SUCCESS:
icon = "https://www.gstatic.com/images/icons/material/system/2x/check_circle_googgreen_48dp.png"
case cbpb.Build_FAILURE, cbpb.Build_INTERNAL_ERROR:
icon = "https://www.gstatic.com/images/icons/material/system/2x/error_red_48dp.png"
case cbpb.Build_TIMEOUT:
icon = "https://www.gstatic.com/images/icons/material/system/2x/hourglass_empty_black_48dp.png"
default:
icon = "https://www.gstatic.com/images/icons/material/system/2x/question_mark_black_48dp.png"
}
logURL, err := notifiers.AddUTMParams(build.LogUrl, notifiers.ChatMedium)
if err != nil {
return nil, fmt.Errorf("failed to add UTM params: %w", err)
}
// Basic card setup
duration := build.GetFinishTime().AsTime().Sub(build.GetStartTime().AsTime())
duration_min, duration_sec := int(duration.Minutes()), int(duration.Seconds())-int(duration.Minutes())*60
duration_fmt := fmt.Sprintf("%d min %d sec", duration_min, duration_sec)
card := &chat.Card{
Header: &chat.CardHeader{
Title: fmt.Sprintf("Build %s Status: %s", build.Id[:8], build.Status),
Subtitle: build.ProjectId,
ImageUrl: icon,
},
Sections: []*chat.Section{
{
Widgets: []*chat.WidgetMarkup{
{
KeyValue: &chat.KeyValue{
TopLabel: "Duration",
Content: duration_fmt,
},
},
},
},
},
}
// Optional section: display trigger information
if build.BuildTriggerId != "" {
log.Infof("Detected a build trigger id: %s", build.BuildTriggerId)
/*
//TODO(glasnt): Get trigger information for Uri links.
// The repo name in `build` does not include the owner information
// You need to inspect the trigger object to get the full repo name and/or the git URI.
ctx := context.Background()
cbapi, _ := cloudbuild.NewClient(ctx)
trigger_info := cbapi.GetBuildTrigger(ctx, &cbpb.GetBuildTriggerRequest{ProjectId: build.ProjectId, TriggerId: build.BuildTriggerId,})
log.Infof("Trigger Repo URI: %s", trigger_info.??)
*/
repo_name := build.Substitutions["REPO_NAME"]
trigger_name := build.Substitutions["TRIGGER_NAME"]
commit := build.Substitutions["SHORT_SHA"]
// Branch, Tag, or None.
branch_tag_label := "Branch"
branch_tag_value := build.Substitutions["BRANCH_NAME"]
if branch_tag_value == "" {
branch_tag_label = "Tag"
branch_tag_value = build.Substitutions["TAG_NAME"]
if branch_tag_value == "" {
branch_tag_label = "Branch/Tag"
branch_tag_value = "[no branch or tag]"
}
}
card.Header.Subtitle = fmt.Sprintf("%s on %s", trigger_name, build.ProjectId)
build_info := &chat.Section{
Header: "Trigger information",
Widgets: []*chat.WidgetMarkup{
{
KeyValue: &chat.KeyValue{
TopLabel: "Trigger",
Content: trigger_name,
},
},
{
KeyValue: &chat.KeyValue{
TopLabel: "Repo",
Content: repo_name,
},
},
{
KeyValue: &chat.KeyValue{
TopLabel: branch_tag_label,
Content: branch_tag_value,
},
},
{
KeyValue: &chat.KeyValue{
TopLabel: "Commit",
Content: commit,
},
},
},
}
card.Sections = append(card.Sections, build_info)
}
// Optional section: display information about errors
if build.FailureInfo != nil {
failure_info := &chat.Section{
Header: "Error information",
Widgets: []*chat.WidgetMarkup{
{
TextParagraph: &chat.TextParagraph{
Text: build.FailureInfo.GetDetail(),
},
},
},
}
card.Sections = append(card.Sections, failure_info)
}
// Append action button
action_section := &chat.Section{
Widgets: []*chat.WidgetMarkup{
{
Buttons: []*chat.Button{
{
TextButton: &chat.TextButton{
Text: "open logs",
OnClick: &chat.OnClick{
OpenLink: &chat.OpenLink{
Url: logURL,
},
},
},
},
},
},
},
}
card.Sections = append(card.Sections, action_section)
msg := chat.Message{Cards: []*chat.Card{card}}
return &msg, nil
}