sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/commentpruner/commentpruner.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package commentpruner facilitates efficiently deleting bot comments as a reaction to webhook events.
    18  package commentpruner
    19  
    20  import (
    21  	"sync"
    22  
    23  	"github.com/sirupsen/logrus"
    24  
    25  	"sigs.k8s.io/prow/pkg/github"
    26  )
    27  
    28  type githubClient interface {
    29  	BotUserChecker() (func(candidate string) bool, error)
    30  	ListIssueComments(org, repo string, number int) ([]github.IssueComment, error)
    31  	DeleteComment(org, repo string, id int) error
    32  }
    33  
    34  // EventClient is a struct that provides bot comment deletion for an event related to an issue.
    35  // A single client instance should be created for each event and shared by all consumers of the event.
    36  // The client fetches the comments only once and filters that list repeatedly to find bot comments to
    37  // delete. This avoids using lots of API tokens when fetching comments for each handler that wants
    38  // to delete comments. (An HTTP cache only partially helps with this because deletions modify the
    39  // list of comments so the next call requires GH to send the resource again.)
    40  type EventClient struct {
    41  	org    string
    42  	repo   string
    43  	number int
    44  
    45  	ghc githubClient
    46  	log *logrus.Entry
    47  
    48  	once     sync.Once
    49  	lock     sync.Mutex
    50  	comments []github.IssueComment
    51  }
    52  
    53  // NewEventClient creates an EventClient struct. This should be used once per webhook event.
    54  func NewEventClient(ghc githubClient, log *logrus.Entry, org, repo string, number int) *EventClient {
    55  	return &EventClient{
    56  		org:    org,
    57  		repo:   repo,
    58  		number: number,
    59  
    60  		ghc: ghc,
    61  		log: log,
    62  	}
    63  }
    64  
    65  // PruneComments fetches issue comments if they have not yet been fetched for this webhook event
    66  // and then deletes any bot comments indicated by the func 'shouldPrune'.
    67  func (c *EventClient) PruneComments(shouldPrune func(github.IssueComment) bool) {
    68  	c.once.Do(func() {
    69  		botUserChecker, err := c.ghc.BotUserChecker()
    70  		if err != nil {
    71  			c.log.WithError(err).Error("failed to get the bot's name. Pruning will consider all comments.")
    72  		}
    73  		comments, err := c.ghc.ListIssueComments(c.org, c.repo, c.number)
    74  		if err != nil {
    75  			c.log.WithError(err).Errorf("failed to list comments for %s/%s#%d", c.org, c.repo, c.number)
    76  		}
    77  		if botUserChecker != nil {
    78  			for _, comment := range comments {
    79  				if botUserChecker(comment.User.Login) {
    80  					c.comments = append(c.comments, comment)
    81  				}
    82  			}
    83  		}
    84  	})
    85  
    86  	c.lock.Lock()
    87  	defer c.lock.Unlock()
    88  
    89  	var remaining []github.IssueComment
    90  	for _, comment := range c.comments {
    91  		removed := false
    92  		if shouldPrune(comment) {
    93  			if err := c.ghc.DeleteComment(c.org, c.repo, comment.ID); err != nil {
    94  				c.log.WithError(err).Errorf("failed to delete stale comment with ID '%d'", comment.ID)
    95  			} else {
    96  				removed = true
    97  			}
    98  		}
    99  		if !removed {
   100  			remaining = append(remaining, comment)
   101  		}
   102  	}
   103  	c.comments = remaining
   104  }