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  }