sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/clonerefs/run.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 clonerefs 18 19 import ( 20 "crypto/md5" 21 "crypto/rsa" 22 "encoding/json" 23 "fmt" 24 "os" 25 "os/exec" 26 "path/filepath" 27 "strings" 28 "sync" 29 30 "github.com/dgrijalva/jwt-go/v4" 31 "github.com/sirupsen/logrus" 32 prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 33 "sigs.k8s.io/prow/pkg/config/secret" 34 "sigs.k8s.io/prow/pkg/github" 35 "sigs.k8s.io/prow/pkg/pod-utils/clone" 36 ) 37 38 var cloneFunc = clone.Run 39 40 func (o *Options) createRecords() []clone.Record { 41 var rec clone.Record 42 var env []string 43 if len(o.KeyFiles) > 0 { 44 var err error 45 var cmds []clone.Command 46 env, cmds, err = addSSHKeys(o.KeyFiles) 47 rec.Commands = append(rec.Commands, cmds...) 48 if err != nil { 49 logrus.WithError(err).Error("Failed to add SSH keys.") 50 rec.Failed = true 51 return []clone.Record{rec} 52 } 53 } 54 if len(o.HostFingerprints) > 0 { 55 envVar, cmds, err := addHostFingerprints(o.HostFingerprints) 56 rec.Commands = append(rec.Commands, cmds...) 57 if err != nil { 58 logrus.WithError(err).Error("failed to add host fingerprints") 59 rec.Failed = true 60 return []clone.Record{rec} 61 } 62 env = append(env, envVar) 63 } 64 65 var userGenerator github.UserGenerator 66 var tokenGenerator github.TokenGenerator 67 if o.OauthTokenFile != "" { 68 if err := secret.Add(o.OauthTokenFile); err != nil { 69 logrus.WithError(err).Error("Failed to read oauth key file.") 70 rec.Failed = true 71 return []clone.Record{rec} 72 } 73 tokenGenerator = func(_ string) (string, error) { 74 return string(secret.GetSecret(o.OauthTokenFile)), nil 75 } 76 } 77 if o.GitHubAppID != "" && o.GitHubAppPrivateKeyFile != "" { 78 if err := secret.Add(o.GitHubAppPrivateKeyFile); err != nil { 79 logrus.WithError(err).Error("Failed to read GitHub App private key file.") 80 rec.Failed = true 81 return []clone.Record{rec} 82 } 83 var err error 84 tokenGenerator, userGenerator, _, err = github.NewClientFromOptions(logrus.Fields{}, github.ClientOptions{ 85 Censor: secret.Censor, 86 AppID: o.GitHubAppID, 87 AppPrivateKey: func() *rsa.PrivateKey { 88 raw := secret.GetSecret(o.GitHubAppPrivateKeyFile) 89 privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(raw) 90 if err != nil { 91 logrus.WithError(err).Error("Failed to parse GitHub App private key.") 92 return nil 93 } 94 return privateKey 95 }, 96 Bases: o.GitHubAPIEndpoints, 97 }) 98 if err != nil { 99 logrus.WithError(err).Error("Failed to construct github client") 100 rec.Failed = true 101 return []clone.Record{rec} 102 } 103 } 104 105 // Print md5 sum of cookiefile for debugging purpose 106 if len(o.CookiePath) > 0 { 107 l := logrus.WithField("http-cookiefile", o.CookiePath) 108 f, err := os.ReadFile(o.CookiePath) 109 if err != nil { 110 l.WithError(err).Warn("Cannot read http cookiefile") 111 } else { 112 l.WithField("md5sum", fmt.Sprintf("%x", md5.Sum(f))).Info("Http cookiefile md5 sum") 113 } 114 } 115 if p := needsGlobalCookiePath(o.CookiePath, o.GitRefs...); p != "" { 116 cmd, err := configureGlobalCookiefile(p) 117 rec.Commands = append(rec.Commands, cmd) 118 if err != nil { 119 logrus.WithError(err).WithField("path", p).Error("Failed to configure global cookiefile") 120 rec.Failed = true 121 return []clone.Record{rec} 122 } 123 } 124 125 var numWorkers int 126 if o.MaxParallelWorkers != 0 { 127 numWorkers = o.MaxParallelWorkers 128 } else { 129 numWorkers = len(o.GitRefs) 130 } 131 132 var wg sync.WaitGroup 133 wg.Add(numWorkers) 134 135 input := make(chan prowapi.Refs) 136 output := make(chan clone.Record, len(o.GitRefs)) 137 for i := 0; i < numWorkers; i++ { 138 go func() { 139 defer wg.Done() 140 for ref := range input { 141 output <- cloneFunc(ref, o.SrcRoot, o.GitUserName, o.GitUserEmail, o.CookiePath, env, userGenerator, tokenGenerator) 142 } 143 }() 144 } 145 146 for _, ref := range o.GitRefs { 147 input <- ref 148 } 149 150 close(input) 151 wg.Wait() 152 close(output) 153 154 results := []clone.Record{rec} 155 for record := range output { 156 results = append(results, record) 157 } 158 return results 159 } 160 161 // Run clones the configured refs 162 func (o Options) Run() error { 163 results := o.createRecords() 164 logData, err := json.Marshal(results) 165 if err != nil { 166 return fmt.Errorf("marshal clone records: %w", err) 167 } 168 169 if err := os.WriteFile(o.Log, logData, 0755); err != nil { 170 return fmt.Errorf("write clone records: %w", err) 171 } 172 173 var failed int 174 for _, record := range results { 175 if record.Failed { 176 failed++ 177 } 178 } 179 180 if o.Fail && failed > 0 { 181 return fmt.Errorf("%d clone records failed", failed) 182 } 183 184 return nil 185 } 186 187 func needsGlobalCookiePath(cookieFile string, refs ...prowapi.Refs) string { 188 if cookieFile == "" || len(refs) == 0 { 189 return "" 190 } 191 192 for _, r := range refs { 193 if !r.SkipSubmodules { 194 return cookieFile 195 } 196 } 197 return "" 198 } 199 200 // configureGlobalCookiefile ensures git authenticates submodules correctly. 201 // 202 // Since this is a global setting, we do it once and before running parallel clones. 203 func configureGlobalCookiefile(cookiePath string) (clone.Command, error) { 204 out, err := exec.Command("git", "config", "--global", "http.cookiefile", cookiePath).CombinedOutput() 205 cmd := clone.Command{ 206 Command: fmt.Sprintf("git config --global http.cookiefile %q", cookiePath), 207 Output: string(out), 208 } 209 if err != nil { 210 cmd.Error = err.Error() 211 } 212 return cmd, err 213 } 214 215 func addHostFingerprints(fingerprints []string) (string, []clone.Command, error) { 216 // let's try to create the tmp dir if it doesn't exist 217 var cmds []clone.Command 218 sshDir := "/tmp" 219 if _, err := os.Stat(sshDir); os.IsNotExist(err) { 220 err := os.MkdirAll(sshDir, 0755) 221 cmd := clone.Command{ 222 Command: fmt.Sprintf("golang: create %q", sshDir), 223 } 224 if err != nil { 225 cmd.Error = err.Error() 226 } 227 cmds = append(cmds, cmd) 228 if err != nil { 229 return "", cmds, fmt.Errorf("create sshDir %s: %w", sshDir, err) 230 } 231 } 232 233 knownHostsFile := filepath.Join(sshDir, "known_hosts") 234 f, err := os.OpenFile(knownHostsFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 235 cmd := clone.Command{ 236 Command: fmt.Sprintf("golang: append %q", knownHostsFile), 237 } 238 if err != nil { 239 cmd.Error = err.Error() 240 cmds = append(cmds, cmd) 241 return "", cmds, fmt.Errorf("append %s: %w", knownHostsFile, err) 242 } 243 244 if _, err := f.Write([]byte(strings.Join(fingerprints, "\n"))); err != nil { 245 cmd.Error = err.Error() 246 cmds = append(cmds, cmd) 247 return "", cmds, fmt.Errorf("write fingerprints to %s: %w", knownHostsFile, err) 248 } 249 if err := f.Close(); err != nil { 250 cmd.Error = err.Error() 251 cmds = append(cmds, cmd) 252 return "", cmds, fmt.Errorf("close %s: %w", knownHostsFile, err) 253 } 254 cmds = append(cmds, cmd) 255 logrus.Infof("Updated known_hosts in file: %s", knownHostsFile) 256 257 ssh, err := exec.LookPath("ssh") 258 cmd = clone.Command{ 259 Command: "golang: lookup ssh path", 260 } 261 262 if err != nil { 263 cmd.Error = err.Error() 264 cmds = append(cmds, cmd) 265 return "", cmds, fmt.Errorf("lookup ssh path: %w", err) 266 } 267 cmds = append(cmds, cmd) 268 return fmt.Sprintf("GIT_SSH_COMMAND=%s -o UserKnownHostsFile=%s", ssh, knownHostsFile), cmds, nil 269 } 270 271 // addSSHKeys will start the ssh-agent and add all the specified 272 // keys, returning the ssh-agent environment variables for reuse 273 func addSSHKeys(paths []string) ([]string, []clone.Command, error) { 274 var cmds []clone.Command 275 vars, err := exec.Command("ssh-agent").CombinedOutput() 276 cmd := clone.Command{ 277 Command: "ssh-agent", 278 Output: string(vars), 279 } 280 if err != nil { 281 cmd.Error = err.Error() 282 } 283 cmds = append(cmds, cmd) 284 if err != nil { 285 return []string{}, cmds, fmt.Errorf("start ssh-agent: %w", err) 286 } 287 logrus.Info("Started SSH agent") 288 // ssh-agent will output three lines of text, in the form: 289 // SSH_AUTH_SOCK=xxx; export SSH_AUTH_SOCK; 290 // SSH_AGENT_PID=xxx; export SSH_AGENT_PID; 291 // echo Agent pid xxx; 292 // We need to parse out the environment variables from that. 293 parts := strings.Split(string(vars), ";") 294 env := []string{strings.TrimSpace(parts[0]), strings.TrimSpace(parts[2])} 295 for _, keyPath := range paths { 296 // we can be given literal paths to keys or paths to dirs 297 // that are mounted from a secret, so we need to check which 298 // we have 299 if err := filepath.Walk(keyPath, func(path string, info os.FileInfo, err error) error { 300 if err != nil { 301 return err 302 } 303 if strings.HasPrefix(info.Name(), "..") { 304 // kubernetes volumes also include files we 305 // should not look be looking into for keys 306 if info.IsDir() { 307 return filepath.SkipDir 308 } 309 return nil 310 } 311 if info.IsDir() { 312 return nil 313 } 314 315 cmd := exec.Command("ssh-add", path) 316 cmd.Env = append(cmd.Env, env...) 317 output, err := cmd.CombinedOutput() 318 cloneCmd := clone.Command{ 319 Command: fmt.Sprintf("ssh-add %q", path), 320 Output: string(output), 321 } 322 if err != nil { 323 cloneCmd.Error = err.Error() 324 } 325 cmds = append(cmds, cloneCmd) 326 if err != nil { 327 return fmt.Errorf("add ssh key at %s: %v: %s", path, err, output) 328 } 329 logrus.Infof("Added SSH key at %s", path) 330 return nil 331 }); err != nil { 332 return env, cmds, fmt.Errorf("walking path %q: %w", keyPath, err) 333 } 334 } 335 return env, cmds, nil 336 }