package externalplugin import ( "encoding/json" "fmt" "net/http" "regexp" "sync" "github.com/sirupsen/logrus" "k8s.io/test-infra/prow/config" "k8s.io/test-infra/prow/git/v2" "k8s.io/test-infra/prow/github" "k8s.io/test-infra/prow/pluginhelp" "k8s.io/test-infra/prow/plugins" "github.com/aws/eks-distro-build-tooling/tools/eksDistroBuildToolingOpsTools/pkg/constants" ) const PluginName = "eksdistroopstool" type githubClient interface { AssignIssue(org, repo string, number int, logins []string) error CreateComment(org, repo string, number int, comment string) error CreateIssue(org, repo, title, body string, milestone int, labels, assignees []string) (int, error) FindIssuesWithOrg(org, query, sort string, asc bool) ([]github.Issue, error) GetIssue(org, repo string, number int) (*github.Issue, error) IsMember(org, user string) (bool, error) } var versionsRe = regexp.MustCompile(fmt.Sprintf(`(?m)(%s)`, constants.SemverRegex)) // HelpProvider construct the pluginhelp.PluginHelp for this plugin. func HelpProvider(_ []config.OrgRepo) (*pluginhelp.PluginHelp, error) { pluginHelp := &pluginhelp.PluginHelp{ Description: `The golang patch release plugin is used for EKS-Distro automation creating issues of upstream Golang security fixes for EKS supported versions. For every successful golang patch release trigger, a new issue is created that mirrors upstream security issues and assigned to the requestor.`, } pluginHelp.AddCommand(pluginhelp.Command{ Usage: "Triggered off issues with |Golang Patch Release: | in title", Description: "Create issue that mirrors security issues when Patch/Security releases are announced.", Featured: true, WhoCanUse: "No use case. Follows automation", Examples: []string{""}, }) return pluginHelp, nil } // Server implements http.Handler. It validates incoming GitHub webhooks and // then dispatches them to the appropriate plugins. type Server struct { TokenGenerator func() []byte BotUser *github.UserData Email string Gc git.ClientFactory Ghc githubClient Log *logrus.Entry // Labels to apply. Labels []string // Use prow to assign users issues. ProwAssignments bool // Allow anybody to request or trigger event. AllowAll bool // Create an issue on conflict. IssueOnConflict bool // Set a custom label prefix. LabelPrefix string Bare *http.Client PatchURL string Repos []github.Repo mapLock sync.Mutex lockGolangPatchMap map[golangPatchReleaseRequest]*sync.Mutex lockBackportMap map[backportRequest]*sync.Mutex } // ServeHTTP validates an incoming webhook and puts it into the event channel. func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { eventType, eventGUID, payload, ok, _ := github.ValidateWebhook(w, r, s.TokenGenerator) if !ok { return } fmt.Fprint(w, "Event received. Have a nice day.") if err := s.handleEvent(eventType, eventGUID, payload); err != nil { logrus.WithError(err).Error("Error parsing event.") } } func (s *Server) handleEvent(eventType, eventGUID string, payload []byte) error { l := logrus.WithFields(logrus.Fields{ "event-type": eventType, github.EventGUID: eventGUID, }) switch eventType { case "issues": var ie github.IssueEvent if err := json.Unmarshal(payload, &ie); err != nil { return err } go func() { if err := s.handleIssue(l, ie); err != nil { s.Log.WithError(err).WithFields(l.Data).Info("Handle Issue Failed.") } }() case "issue_comment": var ic github.IssueCommentEvent if err := json.Unmarshal(payload, &ic); err != nil { return err } go func() { if err := s.handleIssueComment(l, ic); err != nil { s.Log.WithError(err).WithFields(l.Data).Info("Handle Issue Comment Failed.") } }() default: logrus.Debugf("skipping event of type %q", eventType) } return nil } func (s *Server) handleIssue(l *logrus.Entry, ie github.IssueEvent) error { // Only consider newly opened issues and not PRs if ie.Action != github.IssueActionOpened && !ie.Issue.IsPullRequest() { return nil } org := ie.Repo.Owner.Login repo := ie.Repo.Name num := ie.Issue.Number author := ie.Sender.Login title := ie.Issue.Title body := ie.Issue.Body // Do not create a new logger, its fields are re-used by the caller in case of errors *l = *l.WithFields(logrus.Fields{ github.OrgLogField: org, github.RepoLogField: repo, github.PrLogField: num, }) golangPatchMatches := golangPatchReleaseRe.FindAllStringSubmatch(ie.Issue.Title, -1) if len(golangPatchMatches) != 0 { if err := s.handleGolangPatchRelease(l, author, &ie.Issue, org, repo, title, body, num); err != nil { return fmt.Errorf("handle GolangPatchrelease: %w", err) } } //TODO: add golangMinorMatches := golangMinorReleaseRe.FindAllStringSubmatch(ie.Issue.Title, -1) //Regex for thisi is below. //var golangMinorReleaseRe = regexp.MustCompile(`(?m)^(?:Golang Minor Release:)\s+(.+)$`) return nil } func (s *Server) handleIssueComment(l *logrus.Entry, ic github.IssueCommentEvent) error { // Only consider newly opened issues. if ic.Action != github.IssueCommentActionCreated { return nil } org := ic.Repo.Owner.Login repo := ic.Repo.Name num := ic.Issue.Number commentAuthor := ic.Comment.User.Login // Do not create a new logger, its fields are re-used by the caller in case of errors *l = *l.WithFields(logrus.Fields{ github.OrgLogField: org, github.RepoLogField: repo, github.PrLogField: num, }) // backportMatches should hold 3 values: // backportMatches[0] holds the full comment body // backportMatches[1] holds the project // backportMatches[2] holds the versions to backport to unparsed. ("v1.2.2 ...") backportMatches := backportRe.FindStringSubmatch(ic.Comment.Body) versions := versionsRe.FindAllString(backportMatches[2], -1) if len(backportMatches) != 0 && len(backportMatches) == 3 { if err := s.handleBackportRequest(l, commentAuthor, &ic.Comment, &ic.Issue, backportMatches[1], versions, org, repo, num); err != nil { return fmt.Errorf("Handle backport request failure: %w", err) } } return nil } // Created based off plugins.FormatICResponse func FormatIEResponse(ie github.IssueEvent, s string) string { return plugins.FormatResponseRaw(ie.Issue.Title, ie.Issue.HTMLURL, ie.Sender.Login, s) } func (s *Server) createComment(l *logrus.Entry, org, repo string, num int, comment *github.IssueComment, resp string) error { if err := func() error { if comment != nil { return s.Ghc.CreateComment(org, repo, num, plugins.FormatICResponse(*comment, resp)) } return s.Ghc.CreateComment(org, repo, num, fmt.Sprintf("In response to: %s", resp)) }(); err != nil { l.WithError(err).Warn("failed to create comment") return err } logrus.Debug("Created comment") return nil } // createIssue creates an issue on GitHub. func (s *Server) createIssue(l *logrus.Entry, org, repo, title, body string, num int, comment *github.IssueComment, labels, assignees []string) error { issueNum, err := s.Ghc.CreateIssue(org, repo, title, body, 0, labels, assignees) if err != nil { return s.createComment(l, org, repo, num, comment, fmt.Sprintf("new issue could not be created for previous request: %v", err)) } return s.createComment(l, org, repo, num, comment, fmt.Sprintf("new issue created for: #%d", issueNum)) }