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 }