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

     1  /*
     2  Copyright 2020 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 ghhook
    18  
    19  import (
    20  	"bytes"
    21  	"errors"
    22  	"flag"
    23  	"fmt"
    24  	"os"
    25  	"strings"
    26  
    27  	"github.com/sirupsen/logrus"
    28  
    29  	"sigs.k8s.io/prow/pkg/flagutil"
    30  	"sigs.k8s.io/prow/pkg/github"
    31  )
    32  
    33  type Options struct {
    34  	GitHubOptions    flagutil.GitHubOptions
    35  	GitHubHookClient github.HookClient
    36  
    37  	Repos        flagutil.Strings
    38  	HookURL      string
    39  	HMACValue    string
    40  	HMACPath     string
    41  	Events       flagutil.Strings
    42  	ShouldDelete bool
    43  	Confirm      bool
    44  }
    45  
    46  func (o *Options) Validate() error {
    47  	if !o.ShouldDelete && o.HMACPath == "" && o.HMACValue == "" {
    48  		return errors.New("either '--hmac-path' or '--hmac-value' must be specified (only one of them)")
    49  	}
    50  	if !o.ShouldDelete && o.HMACValue != "" && o.HMACPath != "" {
    51  		return errors.New("both '--hmac-path' and '--hmac-value' can not be set at the same time")
    52  	}
    53  	if o.HookURL == "" {
    54  		return errors.New("--hook-url must be set")
    55  	}
    56  	if len(o.Repos.Strings()) == 0 {
    57  		return errors.New("no --repos set")
    58  	}
    59  
    60  	o.GitHubOptions.AllowDirectAccess = true
    61  	var err error
    62  	if err = o.GitHubOptions.Validate(!o.Confirm); err != nil {
    63  		return err
    64  	}
    65  
    66  	return nil
    67  }
    68  
    69  func GetOptions(fs *flag.FlagSet, args []string) (*Options, error) {
    70  	o := Options{}
    71  	o.GitHubOptions.AddFlags(fs)
    72  	o.Events = flagutil.NewStrings(github.AllHookEvents...)
    73  	fs.Var(&o.Events, "event", "Receive hooks for the following events, defaults to [\"*\"] (all events)")
    74  	fs.Var(&o.Repos, "repo", "Add hooks for this org or org/repos")
    75  	fs.StringVar(&o.HookURL, "hook-url", "", "URL to send hooks")
    76  	fs.StringVar(&o.HMACPath, "hmac-path", "", "Path to hmac secret")
    77  	fs.StringVar(&o.HMACValue, "hmac-value", "", "hmac secret value")
    78  	fs.BoolVar(&o.Confirm, "confirm", false, "Apply changes to github")
    79  	fs.BoolVar(&o.ShouldDelete, "delete-webhook", false, "Webhook should be deleted")
    80  	fs.Parse(args)
    81  
    82  	var err error
    83  	if err = o.Validate(); err != nil {
    84  		return nil, err
    85  	}
    86  
    87  	o.GitHubHookClient, err = o.GitHubOptions.GitHubClient(!o.Confirm)
    88  	if err != nil {
    89  		return nil, fmt.Errorf("error creating github client: %w", err)
    90  	}
    91  
    92  	return &o, nil
    93  }
    94  
    95  func (o *Options) hmacValueFromFile() (string, error) {
    96  	b, err := os.ReadFile(o.HMACPath)
    97  	if err != nil {
    98  		return "", fmt.Errorf("read %s: %w", o.HMACPath, err)
    99  	}
   100  	return string(bytes.TrimSpace(b)), nil
   101  }
   102  
   103  func (o *Options) HandleWebhookConfigChange() error {
   104  	var hmac string
   105  	var err error
   106  	// hmac is only needed when we add or edit a webhook
   107  	if !o.ShouldDelete {
   108  		hmac, err = o.hmacValue()
   109  		if err != nil {
   110  			return fmt.Errorf("could not load hmac secret: %w", err)
   111  		}
   112  	}
   113  
   114  	yes := true
   115  	j := "json"
   116  	req := github.HookRequest{
   117  		Name:   "web",
   118  		Active: &yes,
   119  		Config: &github.HookConfig{
   120  			URL:         o.HookURL,
   121  			ContentType: &j,
   122  			Secret:      &hmac,
   123  		},
   124  		Events: o.Events.Strings(),
   125  	}
   126  	for _, orgRepo := range o.Repos.Strings() {
   127  		parts := strings.SplitN(orgRepo, "/", 2)
   128  		var ch changer
   129  		if len(parts) == 1 {
   130  			ch = orgChanger(o.GitHubHookClient)
   131  		} else {
   132  			repo := parts[1]
   133  			ch = repoChanger(o.GitHubHookClient, repo)
   134  		}
   135  
   136  		org := parts[0]
   137  		if err := reconcileHook(ch, org, req, o); err != nil {
   138  			return fmt.Errorf("could not apply hook to %s: %w", orgRepo, err)
   139  		}
   140  	}
   141  	return nil
   142  }
   143  
   144  func reconcileHook(ch changer, org string, req github.HookRequest, o *Options) error {
   145  	hooks, err := ch.lister(org)
   146  	if err != nil {
   147  		return fmt.Errorf("list: %w", err)
   148  	}
   149  	id := findHook(hooks, req.Config.URL)
   150  	if id == nil {
   151  		if o.ShouldDelete {
   152  			logrus.Warnf("The webhook for %q does not exist, skip deletion", req.Config.URL)
   153  			return nil
   154  		}
   155  		_, err := ch.creator(org, req)
   156  		return err
   157  	}
   158  	if o.ShouldDelete {
   159  		return ch.deletor(org, *id, req)
   160  	}
   161  	return ch.editor(org, *id, req)
   162  }
   163  
   164  func findHook(hooks []github.Hook, url string) *int {
   165  	for _, h := range hooks {
   166  		if h.Config.URL == url {
   167  			return &h.ID
   168  		}
   169  	}
   170  	return nil
   171  }
   172  
   173  type changer struct {
   174  	lister  func(org string) ([]github.Hook, error)
   175  	editor  func(org string, id int, req github.HookRequest) error
   176  	creator func(org string, req github.HookRequest) (int, error)
   177  	deletor func(org string, id int, req github.HookRequest) error
   178  }
   179  
   180  func orgChanger(client github.HookClient) changer {
   181  	return changer{
   182  		lister:  client.ListOrgHooks,
   183  		editor:  client.EditOrgHook,
   184  		creator: client.CreateOrgHook,
   185  		deletor: client.DeleteOrgHook,
   186  	}
   187  }
   188  
   189  func repoChanger(client github.HookClient, repo string) changer {
   190  	return changer{
   191  		lister: func(org string) ([]github.Hook, error) {
   192  			return client.ListRepoHooks(org, repo)
   193  		},
   194  		editor: func(org string, id int, req github.HookRequest) error {
   195  			return client.EditRepoHook(org, repo, id, req)
   196  		},
   197  		creator: func(org string, req github.HookRequest) (int, error) {
   198  			return client.CreateRepoHook(org, repo, req)
   199  		},
   200  		deletor: func(org string, id int, req github.HookRequest) error {
   201  			return client.DeleteRepoHook(org, repo, id, req)
   202  		},
   203  	}
   204  }
   205  
   206  func (o *Options) hmacValue() (string, error) {
   207  	if o.HMACValue != "" {
   208  		return o.HMACValue, nil
   209  	}
   210  	hmac, err := o.hmacValueFromFile()
   211  	return hmac, err
   212  }