github.com/abayer/test-infra@v0.0.5/prow/plugins/cat/cat.go (about)

     1  /*
     2  Copyright 2018 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 cat adds cat images to issues in response to a /meow comment
    18  package cat
    19  
    20  import (
    21  	"encoding/xml"
    22  	"errors"
    23  	"fmt"
    24  	"io/ioutil"
    25  	"net/http"
    26  	"net/url"
    27  	"regexp"
    28  	"strings"
    29  	"sync"
    30  	"time"
    31  
    32  	"github.com/sirupsen/logrus"
    33  
    34  	"k8s.io/test-infra/prow/github"
    35  	"k8s.io/test-infra/prow/pluginhelp"
    36  	"k8s.io/test-infra/prow/plugins"
    37  )
    38  
    39  var (
    40  	match = regexp.MustCompile(`(?mi)^/meow( .+)?\s*$`)
    41  	meow  = &realClowder{
    42  		url: "http://thecatapi.com/api/images/get?format=xml&results_per_page=1",
    43  	}
    44  )
    45  
    46  const (
    47  	pluginName = "cat"
    48  )
    49  
    50  func init() {
    51  	plugins.RegisterGenericCommentHandler(pluginName, handleGenericComment, helpProvider)
    52  }
    53  
    54  func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
    55  	// The Config field is omitted because this plugin is not configurable.
    56  	pluginHelp := &pluginhelp.PluginHelp{
    57  		Description: "The cat plugin adds a cat image to an issue in response to the `/meow` command.",
    58  	}
    59  	pluginHelp.AddCommand(pluginhelp.Command{
    60  		Usage:       "/meow",
    61  		Description: "Add a cat image to the issue",
    62  		Featured:    false,
    63  		WhoCanUse:   "Anyone",
    64  		Examples:    []string{"/meow", "/meow caturday"},
    65  	})
    66  	return pluginHelp, nil
    67  }
    68  
    69  type githubClient interface {
    70  	CreateComment(owner, repo string, number int, comment string) error
    71  }
    72  
    73  type clowder interface {
    74  	readCat(string) (string, error)
    75  }
    76  
    77  type realClowder struct {
    78  	url     string
    79  	lock    sync.RWMutex
    80  	update  time.Time
    81  	key     string
    82  	keyPath string
    83  }
    84  
    85  func (c *realClowder) setKey(keyPath string, log *logrus.Entry) {
    86  	c.lock.Lock()
    87  	defer c.lock.Unlock()
    88  	if !time.Now().After(c.update) {
    89  		return
    90  	}
    91  	c.update = time.Now().Add(1 * time.Minute)
    92  	if keyPath == "" {
    93  		c.key = ""
    94  		return
    95  	}
    96  	b, err := ioutil.ReadFile(keyPath)
    97  	if err == nil {
    98  		c.key = strings.TrimSpace(string(b))
    99  		return
   100  	}
   101  	log.WithError(err).Errorf("failed to read key at %s", keyPath)
   102  	c.key = ""
   103  }
   104  
   105  var client = http.Client{}
   106  
   107  type catResult struct {
   108  	Source string `xml:"data>images>image>source_url"`
   109  	Image  string `xml:"data>images>image>url"`
   110  }
   111  
   112  func (cr catResult) Format() (string, error) {
   113  	if cr.Source == "" {
   114  		return "", errors.New("empty source_url")
   115  	}
   116  	if cr.Image == "" {
   117  		return "", errors.New("empty image url")
   118  	}
   119  	src, err := url.Parse(cr.Source)
   120  	if err != nil {
   121  		return "", fmt.Errorf("invalid source_url %s: %v", cr.Source, err)
   122  	}
   123  	img, err := url.Parse(cr.Image)
   124  	if err != nil {
   125  		return "", fmt.Errorf("invalid image url %s: %v", cr.Image, err)
   126  	}
   127  
   128  	return fmt.Sprintf("[![cat image](%s)](%s)", img, src), nil
   129  }
   130  
   131  func (r *realClowder) Url(category string) string {
   132  	r.lock.RLock()
   133  	defer r.lock.RUnlock()
   134  	uri := string(r.url)
   135  	if category != "" {
   136  		uri += "&category=" + url.QueryEscape(category)
   137  	}
   138  	if r.key != "" {
   139  		uri += "&api_key=" + url.QueryEscape(r.key)
   140  	}
   141  	return uri
   142  }
   143  
   144  func (r *realClowder) readCat(category string) (string, error) {
   145  	uri := r.Url(category)
   146  	req, err := http.NewRequest("GET", uri, nil)
   147  	if err != nil {
   148  		return "", fmt.Errorf("could not create request %s: %v", uri, err)
   149  	}
   150  	req.Header.Add("Accept", "application/json")
   151  	resp, err := client.Do(req)
   152  	if err != nil {
   153  		return "", fmt.Errorf("could not read cat from %s: %v", uri, err)
   154  	}
   155  	defer resp.Body.Close()
   156  	var a catResult
   157  	if err = xml.NewDecoder(resp.Body).Decode(&a); err != nil {
   158  		return "", err
   159  	}
   160  	// checking size, GitHub doesn't support big images
   161  	toobig, err := github.ImageTooBig(a.Image)
   162  	if err != nil {
   163  		return "", err
   164  	} else if toobig {
   165  		return "", errors.New("unsupported cat :( size too big: " + a.Image)
   166  	}
   167  	return a.Format()
   168  }
   169  
   170  func handleGenericComment(pc plugins.PluginClient, e github.GenericCommentEvent) error {
   171  	return handle(
   172  		pc.GitHubClient,
   173  		pc.Logger,
   174  		&e,
   175  		meow,
   176  		func() { meow.setKey(pc.PluginConfig.Cat.KeyPath, pc.Logger) },
   177  	)
   178  }
   179  
   180  func handle(gc githubClient, log *logrus.Entry, e *github.GenericCommentEvent, c clowder, setKey func()) error {
   181  	// Only consider new comments.
   182  	if e.Action != github.GenericCommentActionCreated {
   183  		return nil
   184  	}
   185  	// Make sure they are requesting a cat
   186  	mat := match.FindStringSubmatch(e.Body)
   187  	if mat == nil {
   188  		return nil
   189  	}
   190  
   191  	// Now that we know this is a relevant event we can set the key.
   192  	setKey()
   193  
   194  	category := mat[1]
   195  	if len(category) > 1 {
   196  		category = category[1:]
   197  	}
   198  
   199  	org := e.Repo.Owner.Login
   200  	repo := e.Repo.Name
   201  	number := e.Number
   202  
   203  	for i := 0; i < 3; i++ {
   204  		resp, err := c.readCat(category)
   205  		if err != nil {
   206  			log.WithError(err).Error("Failed to get cat img")
   207  			continue
   208  		}
   209  		return gc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, e.User.Login, resp))
   210  	}
   211  
   212  	var msg string
   213  	if category != "" {
   214  		msg = "Bad category. Please see http://thecatapi.com/api/categories/list"
   215  	} else {
   216  		msg = "http://thecatapi.com appears to be down"
   217  	}
   218  	if err := gc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, e.User.Login, msg)); err != nil {
   219  		log.WithError(err).Error("Failed to leave comment")
   220  	}
   221  
   222  	return errors.New("could not find a valid cat image")
   223  }