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 }