github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/plugin/builtin/git/patch.go (about)

     1  package git
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"io/ioutil"
     7  	"net/http"
     8  	"path/filepath"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/evergreen-ci/evergreen/command"
    13  	"github.com/evergreen-ci/evergreen/db"
    14  	"github.com/evergreen-ci/evergreen/model"
    15  	"github.com/evergreen-ci/evergreen/model/patch"
    16  	"github.com/evergreen-ci/evergreen/plugin"
    17  	"github.com/evergreen-ci/evergreen/util"
    18  	"github.com/gorilla/mux"
    19  	"github.com/mongodb/grip"
    20  	"github.com/mongodb/grip/slogger"
    21  	"github.com/pkg/errors"
    22  )
    23  
    24  // GitApplyPatchCommand is deprecated. Its functionality is now a part of GitGetProjectCommand.
    25  type GitApplyPatchCommand struct{}
    26  
    27  func (*GitApplyPatchCommand) Name() string                                    { return ApplyPatchCmdName }
    28  func (*GitApplyPatchCommand) Plugin() string                                  { return GitPluginName }
    29  func (*GitApplyPatchCommand) ParseParams(params map[string]interface{}) error { return nil }
    30  func (*GitApplyPatchCommand) Execute(pluginLogger plugin.Logger,
    31  	pluginCom plugin.PluginCommunicator, conf *model.TaskConfig, stop chan bool) error {
    32  	pluginLogger.LogExecution(slogger.INFO,
    33  		"WARNING: git.apply_patch is deprecated. Patches are applied in git.get_project ")
    34  	return nil
    35  }
    36  
    37  // GetPatch tries to get the patch data from the server in json format,
    38  // and unmarhals it into a patch struct. The GET request is attempted
    39  // multiple times upon failure.
    40  func (ggpc GitGetProjectCommand) GetPatch(pluginCom plugin.PluginCommunicator, pluginLogger plugin.Logger) (*patch.Patch, error) {
    41  	patch := &patch.Patch{}
    42  	retriableGet := func() error {
    43  		resp, err := pluginCom.TaskGetJSON(GitPatchPath)
    44  		if resp != nil {
    45  			defer resp.Body.Close()
    46  		}
    47  
    48  		if err != nil {
    49  			//Some generic error trying to connect - try again
    50  			pluginLogger.LogExecution(slogger.WARN, "Error connecting to API server: %v", err)
    51  			return util.RetriableError{err}
    52  		}
    53  
    54  		if resp != nil && resp.StatusCode == http.StatusNotFound {
    55  			//nothing broke, but no patch was found for task Id - no retry
    56  			body, err := ioutil.ReadAll(resp.Body)
    57  			if err != nil {
    58  				pluginLogger.LogExecution(slogger.ERROR, "Error reading response body")
    59  			}
    60  			msg := fmt.Sprintf("no patch found for task: %v", string(body))
    61  			pluginLogger.LogExecution(slogger.WARN, msg)
    62  			return errors.New(msg)
    63  		}
    64  
    65  		if resp != nil && resp.StatusCode == http.StatusInternalServerError {
    66  			//something went wrong in api server
    67  			body, err := ioutil.ReadAll(resp.Body)
    68  			if err != nil {
    69  				pluginLogger.LogExecution(slogger.ERROR, "Error reading response body")
    70  			}
    71  			msg := fmt.Sprintf("error fetching patch from server: %v", string(body))
    72  			pluginLogger.LogExecution(slogger.WARN, msg)
    73  			return util.RetriableError{
    74  				errors.New(msg),
    75  			}
    76  		}
    77  
    78  		if resp != nil && resp.StatusCode == http.StatusConflict {
    79  			//wrong secret
    80  			body, err := ioutil.ReadAll(resp.Body)
    81  			if err != nil {
    82  				pluginLogger.LogExecution(slogger.ERROR, "Error reading response body")
    83  			}
    84  			msg := fmt.Sprintf("secret conflict: %v", string(body))
    85  			pluginLogger.LogExecution(slogger.ERROR, msg)
    86  			return errors.New(msg)
    87  		}
    88  
    89  		if resp == nil {
    90  			pluginLogger.LogExecution(slogger.WARN, "Empty response from API server")
    91  			return util.RetriableError{errors.New("empty response")}
    92  		}
    93  
    94  		err = util.ReadJSONInto(resp.Body, patch)
    95  		if err != nil {
    96  			pluginLogger.LogExecution(slogger.ERROR,
    97  				"Error reading json into patch struct: %v", err)
    98  			return util.RetriableError{err}
    99  		}
   100  		return nil
   101  	}
   102  
   103  	retryFail, err := util.Retry(retriableGet, 10, 1*time.Second)
   104  	if retryFail {
   105  		return nil, errors.Wrapf(err, "getting patch failed after %d tries", 10)
   106  	}
   107  	if err != nil {
   108  		return nil, errors.Wrap(err, "getting patch failed")
   109  	}
   110  	return patch, nil
   111  }
   112  
   113  // getPatchContents() dereferences any patch files that are stored externally, fetching them from
   114  // the API server, and setting them into the patch object.
   115  func (ggpc GitGetProjectCommand) getPatchContents(com plugin.PluginCommunicator, log plugin.Logger, p *patch.Patch) error {
   116  	for i, patchPart := range p.Patches {
   117  		// If the patch isn't stored externally, no need to do anything.
   118  		if patchPart.PatchSet.PatchFileId == "" {
   119  			continue
   120  		}
   121  		// otherwise, fetch the contents and load it into the patch object
   122  		log.LogExecution(slogger.INFO, "Fetching patch contents for %v", patchPart.PatchSet.PatchFileId)
   123  		var result []byte
   124  		retriableGet := util.RetriableFunc(
   125  			func() error {
   126  				resp, err := com.TaskGetJSON(fmt.Sprintf("%s/%s", GitPatchFilePath, patchPart.PatchSet.PatchFileId))
   127  				if resp != nil {
   128  					defer resp.Body.Close()
   129  				}
   130  				if err != nil {
   131  					//Some generic error trying to connect - try again
   132  					log.LogExecution(slogger.WARN, "Error connecting to API server: %v", err)
   133  					return util.RetriableError{err}
   134  				}
   135  				if resp != nil && resp.StatusCode != http.StatusOK {
   136  					log.LogExecution(slogger.WARN, "Unexpected status code %v, retrying", resp.StatusCode)
   137  					_ = resp.Body.Close()
   138  					return util.RetriableError{errors.Errorf("Unexpected status code %v", resp.StatusCode)}
   139  				}
   140  				result, err = ioutil.ReadAll(resp.Body)
   141  
   142  				return err
   143  			})
   144  
   145  		_, err := util.Retry(retriableGet, 10, 1*time.Second)
   146  		if err != nil {
   147  			return err
   148  		}
   149  		p.Patches[i].PatchSet.Patch = string(result)
   150  	}
   151  	return nil
   152  }
   153  
   154  // GetPatchCommands, given a module patch of a patch, will return the appropriate list of commands that
   155  // need to be executed. If the patch is empty it will not apply the patch.
   156  func GetPatchCommands(modulePatch patch.ModulePatch, dir, patchPath string) []string {
   157  	patchCommands := []string{
   158  		fmt.Sprintf("set -o verbose"),
   159  		fmt.Sprintf("set -o errexit"),
   160  		fmt.Sprintf("ls"),
   161  		fmt.Sprintf("cd '%s'", dir),
   162  		fmt.Sprintf("git reset --hard '%s'", modulePatch.Githash),
   163  	}
   164  	if modulePatch.PatchSet.Patch == "" {
   165  		return patchCommands
   166  	}
   167  	return append(patchCommands, []string{
   168  		fmt.Sprintf("git apply --check --whitespace=fix '%v'", patchPath),
   169  		fmt.Sprintf("git apply --stat '%v'", patchPath),
   170  		fmt.Sprintf("git apply --whitespace=fix < '%v'", patchPath),
   171  	}...)
   172  }
   173  
   174  // applyPatch is used by the agent to copy patch data onto disk
   175  // and then call the necessary git commands to apply the patch file
   176  func (ggpc *GitGetProjectCommand) applyPatch(conf *model.TaskConfig,
   177  	p *patch.Patch, pluginLogger plugin.Logger) error {
   178  	// patch sets and contain multiple patches, some of them for modules
   179  	for _, patchPart := range p.Patches {
   180  		var dir string
   181  		if patchPart.ModuleName == "" {
   182  			// if patch is not part of a module, just apply patch against src root
   183  			dir = ggpc.Directory
   184  			pluginLogger.LogExecution(slogger.INFO, "Applying patch with git...")
   185  		} else {
   186  			// if patch is part of a module, apply patch in module root
   187  			module, err := conf.Project.GetModuleByName(patchPart.ModuleName)
   188  			if err != nil {
   189  				return errors.Wrap(err, "Error getting module")
   190  			}
   191  			if module == nil {
   192  				return errors.Errorf("Module '%s' not found", patchPart.ModuleName)
   193  			}
   194  
   195  			// skip the module if this build variant does not use it
   196  			if !util.SliceContains(conf.BuildVariant.Modules, module.Name) {
   197  				pluginLogger.LogExecution(slogger.INFO, "Skipping patch for"+
   198  					" module %v, since the current build variant does not"+
   199  					" use it", module.Name)
   200  				continue
   201  			}
   202  
   203  			dir = filepath.Join(ggpc.Directory, module.Prefix, module.Name)
   204  			pluginLogger.LogExecution(slogger.INFO, "Applying module patch with git...")
   205  		}
   206  
   207  		// create a temporary folder and store patch files on disk,
   208  		// for later use in shell script
   209  		tempFile, err := ioutil.TempFile("", "mcipatch_")
   210  		if err != nil {
   211  			return errors.WithStack(err)
   212  		}
   213  		defer tempFile.Close()
   214  		_, err = io.WriteString(tempFile, patchPart.PatchSet.Patch)
   215  		if err != nil {
   216  			return errors.WithStack(err)
   217  		}
   218  		tempAbsPath := tempFile.Name()
   219  
   220  		// this applies the patch using the patch files in the temp directory
   221  		patchCommandStrings := GetPatchCommands(patchPart, dir, tempAbsPath)
   222  		cmdsJoined := strings.Join(patchCommandStrings, "\n")
   223  		patchCmd := &command.LocalCommand{
   224  			CmdString:        cmdsJoined,
   225  			WorkingDirectory: conf.WorkDir,
   226  			Stdout:           pluginLogger.GetTaskLogWriter(slogger.INFO),
   227  			Stderr:           pluginLogger.GetTaskLogWriter(slogger.ERROR),
   228  			ScriptMode:       true,
   229  		}
   230  
   231  		if err = patchCmd.Run(); err != nil {
   232  			return errors.WithStack(err)
   233  		}
   234  		pluginLogger.Flush()
   235  	}
   236  	return nil
   237  }
   238  
   239  // servePatch is the API hook for returning patch data as json
   240  func servePatch(w http.ResponseWriter, r *http.Request) {
   241  	task := plugin.GetTask(r)
   242  	patch, err := patch.FindOne(patch.ByVersion(task.Version))
   243  	if err != nil {
   244  		msg := fmt.Sprintf("error fetching patch for task %v from db: %v", task.Id, err)
   245  		grip.Error(msg)
   246  		http.Error(w, msg, http.StatusInternalServerError)
   247  		return
   248  	}
   249  	if patch == nil {
   250  		msg := fmt.Sprintf("no patch found for task %v", task.Id)
   251  		grip.Error(msg)
   252  		http.Error(w, msg, http.StatusNotFound)
   253  		return
   254  	}
   255  	plugin.WriteJSON(w, http.StatusOK, patch)
   256  }
   257  
   258  // servePatchFile is the API hook for returning raw patch contents
   259  func servePatchFile(w http.ResponseWriter, r *http.Request) {
   260  	fileId := mux.Vars(r)["patchfile_id"]
   261  	data, err := db.GetGridFile(patch.GridFSPrefix, fileId)
   262  	if err != nil {
   263  		http.Error(w, fmt.Sprintf("Error reading file from db: %v", err), http.StatusInternalServerError)
   264  		return
   265  	}
   266  	defer data.Close()
   267  	_, _ = io.Copy(w, data)
   268  }