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