tools/commentMonitor/main.go (163 lines of code) (raw):
// Copyright 2019 The Prometheus Authors
// 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 (
"context"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/google/go-github/v29/github"
"gopkg.in/alecthomas/kingpin.v2"
"gopkg.in/yaml.v2"
)
type commentMonitorConfig struct {
configFilePath string
whSecretFilePath string
whSecret []byte
configFile configFile
port string
}
type commandPrefix struct {
Prefix string `yaml:"prefix"`
HelpTemplate string `yaml:"help_template"`
VerifyUser bool `yaml:"verify_user"`
}
type webhookEvent struct {
EventType string `yaml:"event_type"`
CommentTemplate string `yaml:"comment_template"`
RegexString string `yaml:"regex_string"`
Label string `yaml:"label"`
}
type configFile struct {
Prefixes []commandPrefix `yaml:"prefixes"`
WebhookEvents []webhookEvent `yaml:"events"`
}
func main() {
log.SetFlags(log.Ltime | log.Lshortfile)
cmConfig := commentMonitorConfig{}
app := kingpin.New(filepath.Base(os.Args[0]), `commentMonitor GithubAction - Post and monitor GitHub comments.`)
app.HelpFlag.Short('h')
app.Flag("webhooksecretfile", "path to webhook secret file").
Default("./whsecret").
StringVar(&cmConfig.whSecretFilePath)
app.Flag("config", "Filepath to config file.").
Default("./config.yml").
StringVar(&cmConfig.configFilePath)
app.Flag("port", "port number to run webhook in.").
Default("8080").
StringVar(&cmConfig.port)
kingpin.MustParse(app.Parse(os.Args[1:]))
mux := http.NewServeMux()
mux.HandleFunc("/", cmConfig.webhookExtract)
log.Println("Server is ready to handle requests at", cmConfig.port)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", cmConfig.port), mux))
}
func (c *commentMonitorConfig) loadConfig() error {
// Get config file.
data, err := os.ReadFile(c.configFilePath)
if err != nil {
return err
}
err = yaml.UnmarshalStrict(data, &c.configFile)
if err != nil {
return fmt.Errorf("cannot unmarshal data: %v", err)
}
if len(c.configFile.WebhookEvents) == 0 || len(c.configFile.Prefixes) == 0 {
return fmt.Errorf("empty eventmap or prefix list")
}
// Get webhook secret.
c.whSecret, err = os.ReadFile(c.whSecretFilePath)
if err != nil {
return err
}
return nil
}
func extractCommand(s string) string {
s = strings.TrimLeft(s, "\r\n\t ")
if i := strings.Index(s, "\n"); i != -1 {
s = s[:i]
}
s = strings.TrimRight(s, "\r\n\t ")
return s
}
func (c *commentMonitorConfig) webhookExtract(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
// Load config on every request.
err := c.loadConfig()
if err != nil {
log.Println(err)
http.Error(w, "comment-monitor configuration incorrect", http.StatusInternalServerError)
return
}
// Validate payload.
payload, err := github.ValidatePayload(r, c.whSecret)
if err != nil {
log.Println(err)
http.Error(w, "unable to read webhook body", http.StatusBadRequest)
return
}
// Setup commentMonitor client.
cmClient := commentMonitorClient{
allArgs: make(map[string]string),
events: c.configFile.WebhookEvents,
prefixes: c.configFile.Prefixes,
}
// Parse webhook event.
event, err := github.ParseWebHook(github.WebHookType(r), payload)
if err != nil {
log.Println(err)
http.Error(w, "unable to parse webhook", http.StatusBadRequest)
return
}
switch e := event.(type) {
case *github.IssueCommentEvent:
if *e.Action != "created" {
http.Error(w, "issue_comment type must be 'created'", http.StatusOK)
return
}
// Setup github client.
ctx := context.Background()
cmClient.ghClient, err = newGithubClient(ctx, e)
if err != nil {
log.Println(err)
http.Error(w, "could not create GitHub client", http.StatusBadRequest)
return
}
// Strip whitespace.
command := extractCommand(cmClient.ghClient.commentBody)
// Command check.
if !cmClient.checkCommandPrefix(command) {
http.Error(w, "comment validation failed", http.StatusOK)
return
}
// Validate regex.
if !cmClient.validateRegex(command) {
log.Println("invalid command syntax: ", command)
err = cmClient.generateAndPostErrorComment()
if err != nil {
log.Println(err)
http.Error(w, "could not post comment to GitHub", http.StatusBadRequest)
return
}
http.Error(w, "command syntax invalid", http.StatusBadRequest)
return
}
// Verify user.
err = cmClient.verifyUser()
if err != nil {
log.Println(err)
http.Error(w, "user not allowed to run command", http.StatusForbidden)
return
}
// Extract args.
err = cmClient.extractArgs(command)
if err != nil {
log.Println(err)
http.Error(w, "could not extract arguments", http.StatusBadRequest)
return
}
// Post generated comment to GitHub pr.
err = cmClient.generateAndPostSuccessComment()
if err != nil {
log.Println(err)
http.Error(w, "could not post comment to GitHub", http.StatusBadRequest)
return
}
// Set label to GitHub pr.
err = cmClient.postLabel()
if err != nil {
log.Println(err)
http.Error(w, "could not set label to GitHub", http.StatusBadRequest)
return
}
default:
log.Println("only issue_comment event is supported")
}
}