github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/model/patch_lifecycle.go (about) 1 package model 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "os" 7 "path/filepath" 8 "strings" 9 "time" 10 11 "github.com/evergreen-ci/evergreen" 12 "github.com/evergreen-ci/evergreen/command" 13 "github.com/evergreen-ci/evergreen/model/build" 14 "github.com/evergreen-ci/evergreen/model/patch" 15 "github.com/evergreen-ci/evergreen/model/task" 16 "github.com/evergreen-ci/evergreen/model/version" 17 "github.com/evergreen-ci/evergreen/thirdparty" 18 "github.com/evergreen-ci/evergreen/util" 19 "github.com/mongodb/grip" 20 "github.com/pkg/errors" 21 "gopkg.in/mgo.v2/bson" 22 "gopkg.in/yaml.v2" 23 ) 24 25 // VariantTasksToTVPairs takes a set of variants and tasks (from both the old and new 26 // request formats) and builds a universal set of pairs 27 // that can be used to expand the dependency tree. 28 func VariantTasksToTVPairs(in []patch.VariantTasks) []TVPair { 29 out := []TVPair{} 30 for _, vt := range in { 31 for _, t := range vt.Tasks { 32 out = append(out, TVPair{vt.Variant, t}) 33 } 34 } 35 return out 36 } 37 38 // TVPairsToVariantTasks takes a list of TVPairs (task/variant pairs), groups the tasks 39 // for the same variant together under a single list, and return all the variant groups 40 // as a set of patch.VariantTasks. 41 func TVPairsToVariantTasks(in []TVPair) []patch.VariantTasks { 42 vtMap := map[string]patch.VariantTasks{} 43 for _, pair := range in { 44 vt := vtMap[pair.Variant] 45 vt.Variant = pair.Variant 46 vt.Tasks = append(vt.Tasks, pair.TaskName) 47 vtMap[pair.Variant] = vt 48 } 49 vts := []patch.VariantTasks{} 50 for _, vt := range vtMap { 51 vts = append(vts, vt) 52 } 53 return vts 54 } 55 56 // ValidateTVPairs checks that all of a set of variant/task pairs exist in a given project. 57 func ValidateTVPairs(p *Project, in []TVPair) error { 58 for _, pair := range in { 59 v := p.FindTaskForVariant(pair.TaskName, pair.Variant) 60 if v == nil { 61 return errors.Errorf("does not exist: task %v, variant %v", pair.TaskName, pair.Variant) 62 } 63 } 64 return nil 65 } 66 67 // Given a patch version and a list of variant/task pairs, creates the set of new builds that 68 // do not exist yet out of the set of pairs. No tasks are added for builds which already exist 69 // (see AddNewTasksForPatch). 70 func AddNewBuildsForPatch(p *patch.Patch, patchVersion *version.Version, project *Project, pairs TVPairSet) error { 71 tt := NewPatchTaskIdTable(project, patchVersion, pairs) 72 73 newBuildIds := make([]string, 0) 74 newBuildStatuses := make([]version.BuildStatus, 0) 75 76 existingBuilds, err := build.Find(build.ByVersion(patchVersion.Id).WithFields(build.BuildVariantKey, build.IdKey)) 77 if err != nil { 78 return err 79 } 80 variantsProcessed := map[string]bool{} 81 for _, b := range existingBuilds { 82 variantsProcessed[b.BuildVariant] = true 83 } 84 85 for _, pair := range pairs { 86 if _, ok := variantsProcessed[pair.Variant]; ok { // skip variant that was already processed 87 continue 88 } 89 variantsProcessed[pair.Variant] = true 90 // Extract the unique set of task names for the variant we're about to create 91 taskNames := pairs.TaskNames(pair.Variant) 92 if len(taskNames) == 0 { 93 continue 94 } 95 buildId, err := CreateBuildFromVersion(project, patchVersion, tt, pair.Variant, p.Activated, taskNames) 96 grip.Infof("Creating build for version %s, buildVariant %s, activated=%t", 97 patchVersion.Id, pair.Variant, p.Activated) 98 if err != nil { 99 return err 100 } 101 newBuildIds = append(newBuildIds, buildId) 102 newBuildStatuses = append(newBuildStatuses, 103 version.BuildStatus{ 104 BuildVariant: pair.Variant, 105 BuildId: buildId, 106 Activated: p.Activated, 107 }, 108 ) 109 } 110 111 return version.UpdateOne( 112 bson.M{version.IdKey: patchVersion.Id}, 113 bson.M{ 114 "$push": bson.M{ 115 version.BuildIdsKey: bson.M{"$each": newBuildIds}, 116 version.BuildVariantsKey: bson.M{"$each": newBuildStatuses}, 117 }, 118 }, 119 ) 120 } 121 122 // Given a patch version and set of variant/task pairs, creates any tasks that don't exist yet, 123 // within the set of already existing builds. 124 func AddNewTasksForPatch(p *patch.Patch, patchVersion *version.Version, project *Project, pairs TVPairSet) error { 125 builds, err := build.Find(build.ByIds(patchVersion.BuildIds).WithFields(build.IdKey, build.BuildVariantKey)) 126 if err != nil { 127 return err 128 } 129 130 for _, b := range builds { 131 // Find the set of task names that already exist for the given build 132 tasksInBuild, err := task.Find(task.ByBuildId(b.Id).WithFields(task.DisplayNameKey)) 133 if err != nil { 134 return err 135 } 136 // build an index to keep track of which tasks already exist 137 existingTasksIndex := map[string]bool{} 138 for _, t := range tasksInBuild { 139 existingTasksIndex[t.DisplayName] = true 140 } 141 // if the patch is activated, treat the build as activated 142 b.Activated = p.Activated 143 144 // build a list of tasks that haven't been created yet for the given variant, but have 145 // a record in the TVPairSet indicating that it should exist 146 tasksToAdd := []string{} 147 for _, taskname := range pairs.TaskNames(b.BuildVariant) { 148 if _, ok := existingTasksIndex[taskname]; ok { 149 continue 150 } 151 tasksToAdd = append(tasksToAdd, taskname) 152 } 153 if len(tasksToAdd) == 0 { // no tasks to add, so we do nothing. 154 continue 155 } 156 // Add the new set of tasks to the build. 157 if _, err = AddTasksToBuild(&b, project, patchVersion, tasksToAdd); err != nil { 158 return err 159 } 160 } 161 return nil 162 } 163 164 // IncludePatchDependencies takes a project and a slice of variant/task pairs names 165 // and returns the expanded set of variant/task pairs to include all the dependencies/requirements 166 // for the given set of tasks. 167 // If any dependency is cross-variant, it will include the variant and task for that dependency. 168 func IncludePatchDependencies(project *Project, tvpairs []TVPair) []TVPair { 169 di := &dependencyIncluder{Project: project} 170 return di.Include(tvpairs) 171 } 172 173 // MakePatchedConfig takes in the path to a remote configuration a stringified version 174 // of the current project and returns an unmarshalled version of the project 175 // with the patch applied 176 func MakePatchedConfig(p *patch.Patch, remoteConfigPath, projectConfig string) ( 177 *Project, error) { 178 for _, patchPart := range p.Patches { 179 // we only need to patch the main project and not any other modules 180 if patchPart.ModuleName != "" { 181 continue 182 } 183 // write patch file 184 patchFilePath, err := util.WriteToTempFile(patchPart.PatchSet.Patch) 185 if err != nil { 186 return nil, errors.Wrap(err, "could not write patch file") 187 } 188 defer os.Remove(patchFilePath) 189 // write project configuration 190 configFilePath, err := util.WriteToTempFile(projectConfig) 191 if err != nil { 192 return nil, errors.Wrap(err, "could not write config file") 193 } 194 defer os.Remove(configFilePath) 195 196 // clean the working directory 197 workingDirectory := filepath.Dir(patchFilePath) 198 localConfigPath := filepath.Join( 199 workingDirectory, 200 remoteConfigPath, 201 ) 202 parentDir := strings.Split( 203 remoteConfigPath, 204 string(os.PathSeparator), 205 )[0] 206 err = os.RemoveAll(filepath.Join(workingDirectory, parentDir)) 207 if err != nil { 208 return nil, errors.WithStack(err) 209 } 210 if err = os.MkdirAll(filepath.Dir(localConfigPath), 0755); err != nil { 211 return nil, errors.WithStack(err) 212 } 213 // rename the temporary config file name to the remote config 214 // file path if we are patching an existing remote config 215 if len(projectConfig) > 0 { 216 if err = os.Rename(configFilePath, localConfigPath); err != nil { 217 return nil, errors.Wrapf(err, "could not rename file '%v' to '%v'", 218 configFilePath, localConfigPath) 219 } 220 defer os.Remove(localConfigPath) 221 } 222 223 // selectively apply the patch to the config file 224 patchCommandStrings := []string{ 225 fmt.Sprintf("set -o verbose"), 226 fmt.Sprintf("set -o errexit"), 227 fmt.Sprintf("git apply --whitespace=fix --include=%v < '%v'", 228 remoteConfigPath, patchFilePath), 229 } 230 231 patchCmd := &command.LocalCommand{ 232 CmdString: strings.Join(patchCommandStrings, "\n"), 233 WorkingDirectory: workingDirectory, 234 Stdout: evergreen.NewInfoLoggingWriter(&evergreen.Logger), 235 Stderr: evergreen.NewErrorLoggingWriter(&evergreen.Logger), 236 ScriptMode: true, 237 } 238 239 if err = patchCmd.Run(); err != nil { 240 return nil, errors.Errorf("could not run patch command: %v", err) 241 } 242 // read in the patched config file 243 data, err := ioutil.ReadFile(localConfigPath) 244 if err != nil { 245 return nil, errors.Wrap(err, "could not read patched config file") 246 } 247 project := &Project{} 248 if err = LoadProjectInto(data, p.Project, project); err != nil { 249 return nil, errors.WithStack(err) 250 } 251 return project, nil 252 } 253 return nil, errors.New("no patch on project") 254 } 255 256 // Finalizes a patch: 257 // Patches a remote project's configuration file if needed. 258 // Creates a version for this patch and links it. 259 // Creates builds based on the version. 260 func FinalizePatch(p *patch.Patch, settings *evergreen.Settings) (*version.Version, error) { 261 // unmarshal the project YAML for storage 262 project := &Project{} 263 err := yaml.Unmarshal([]byte(p.PatchedConfig), project) 264 if err != nil { 265 return nil, errors.Wrapf(err, 266 "Error marshaling patched project config from repository revision ā%vā", 267 p.Githash) 268 } 269 270 projectRef, err := FindOneProjectRef(p.Project) 271 if err != nil { 272 return nil, errors.WithStack(err) 273 } 274 275 gitCommit, err := thirdparty.GetCommitEvent( 276 settings.Credentials["github"], 277 projectRef.Owner, projectRef.Repo, p.Githash, 278 ) 279 if err != nil { 280 return nil, errors.Wrap(err, "Couldn't fetch commit information") 281 } 282 if gitCommit == nil { 283 return nil, errors.New("Couldn't fetch commit information; git commit doesn't exist") 284 } 285 286 patchVersion := &version.Version{ 287 Id: p.Id.Hex(), 288 CreateTime: time.Now(), 289 Identifier: p.Project, 290 Revision: p.Githash, 291 Author: p.Author, 292 Message: p.Description, 293 BuildIds: []string{}, 294 BuildVariants: []version.BuildStatus{}, 295 Config: p.PatchedConfig, 296 Status: evergreen.PatchCreated, 297 Requester: evergreen.PatchVersionRequester, 298 Branch: projectRef.Branch, 299 } 300 301 var pairs []TVPair 302 if len(p.VariantsTasks) > 0 { 303 pairs = VariantTasksToTVPairs(p.VariantsTasks) 304 } else { 305 // handle case where the patch is being finalized but only has the old schema tasks/variants 306 // instead of the new one. 307 for _, v := range p.BuildVariants { 308 for _, t := range p.Tasks { 309 if project.FindTaskForVariant(t, v) != nil { 310 pairs = append(pairs, TVPair{v, t}) 311 } 312 } 313 } 314 p.VariantsTasks = TVPairsToVariantTasks(pairs) 315 } 316 317 tt := NewPatchTaskIdTable(project, patchVersion, pairs) 318 variantsProcessed := map[string]bool{} 319 for _, vt := range p.VariantsTasks { 320 if _, ok := variantsProcessed[vt.Variant]; ok { 321 continue 322 } 323 buildId, err := CreateBuildFromVersion(project, patchVersion, tt, vt.Variant, true, vt.Tasks) 324 if err != nil { 325 return nil, errors.WithStack(err) 326 } 327 patchVersion.BuildIds = append(patchVersion.BuildIds, buildId) 328 patchVersion.BuildVariants = append(patchVersion.BuildVariants, 329 version.BuildStatus{ 330 BuildVariant: vt.Variant, 331 Activated: true, 332 BuildId: buildId, 333 }, 334 ) 335 } 336 337 if err = patchVersion.Insert(); err != nil { 338 return nil, errors.WithStack(err) 339 } 340 if err = p.SetActivated(patchVersion.Id); err != nil { 341 return nil, errors.WithStack(err) 342 } 343 return patchVersion, nil 344 } 345 346 func CancelPatch(p *patch.Patch, caller string) error { 347 if p.Version != "" { 348 if err := SetVersionActivation(p.Version, false, caller); err != nil { 349 return errors.WithStack(err) 350 } 351 return errors.WithStack(AbortVersion(p.Version)) 352 } 353 354 return errors.WithStack(patch.Remove(patch.ById(p.Id))) 355 }