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  }