github.com/ssube/gitlab-ci-multi-runner@v1.2.1-0.20160607142738-b8d1285632e6/common/build.go (about)

     1  package common
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"net/url"
     7  	"os"
     8  	"path"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/Sirupsen/logrus"
    13  	"gitlab.com/gitlab-org/gitlab-ci-multi-runner/helpers"
    14  	"time"
    15  )
    16  
    17  type BuildState string
    18  
    19  const (
    20  	Pending BuildState = "pending"
    21  	Running            = "running"
    22  	Failed             = "failed"
    23  	Success            = "success"
    24  )
    25  
    26  type Build struct {
    27  	GetBuildResponse `yaml:",inline"`
    28  
    29  	Trace        BuildTrace
    30  	BuildAbort   chan os.Signal `json:"-" yaml:"-"`
    31  	RootDir      string         `json:"-" yaml:"-"`
    32  	BuildDir     string         `json:"-" yaml:"-"`
    33  	CacheDir     string         `json:"-" yaml:"-"`
    34  	Hostname     string         `json:"-" yaml:"-"`
    35  	Runner       *RunnerConfig  `json:"runner"`
    36  	ExecutorData ExecutorData
    37  
    38  	// Unique ID for all running builds on this runner
    39  	RunnerID int `json:"runner_id"`
    40  
    41  	// Unique ID for all running builds on this runner and this project
    42  	ProjectRunnerID int `json:"project_runner_id"`
    43  }
    44  
    45  func (b *Build) log() *logrus.Entry {
    46  	return b.Runner.Log().WithField("build", b.ID)
    47  }
    48  
    49  func (b *Build) ProjectUniqueName() string {
    50  	return fmt.Sprintf("runner-%s-project-%d-concurrent-%d",
    51  		b.Runner.ShortDescription(), b.ProjectID, b.ProjectRunnerID)
    52  }
    53  
    54  func (b *Build) ProjectSlug() (string, error) {
    55  	url, err := url.Parse(b.RepoURL)
    56  	if err != nil {
    57  		return "", err
    58  	}
    59  	if url.Host == "" {
    60  		return "", errors.New("only URI reference supported")
    61  	}
    62  
    63  	slug := url.Path
    64  	slug = strings.TrimSuffix(slug, ".git")
    65  	slug = path.Clean(slug)
    66  	if slug == "." {
    67  		return "", errors.New("invalid path")
    68  	}
    69  	if strings.Contains(slug, "..") {
    70  		return "", errors.New("it doesn't look like a valid path")
    71  	}
    72  	return slug, nil
    73  }
    74  
    75  func (b *Build) ProjectUniqueDir(sharedDir bool) string {
    76  	dir, err := b.ProjectSlug()
    77  	if err != nil {
    78  		dir = fmt.Sprintf("project-%d", b.ProjectID)
    79  	}
    80  
    81  	// for shared dirs path is constructed like this:
    82  	// <some-path>/runner-short-id/concurrent-id/group-name/project-name/
    83  	// ex.<some-path>/01234567/0/group/repo/
    84  	if sharedDir {
    85  		dir = path.Join(
    86  			fmt.Sprintf("%s", b.Runner.ShortDescription()),
    87  			fmt.Sprintf("%d", b.ProjectRunnerID),
    88  			dir,
    89  		)
    90  	}
    91  	return dir
    92  }
    93  
    94  func (b *Build) FullProjectDir() string {
    95  	return helpers.ToSlash(b.BuildDir)
    96  }
    97  
    98  func (b *Build) StartBuild(rootDir, cacheDir string, sharedDir bool) {
    99  	b.RootDir = rootDir
   100  	b.BuildDir = path.Join(rootDir, b.ProjectUniqueDir(sharedDir))
   101  	b.CacheDir = path.Join(cacheDir, b.ProjectUniqueDir(false))
   102  }
   103  
   104  func (b *Build) executeScript(buildScript *ShellScript, executor Executor, abort chan interface{}) error {
   105  	// Execute pre script (git clone, cache restore, artifacts download)
   106  	err := executor.Run(ExecutorCommand{
   107  		Script:     buildScript.PreScript,
   108  		Predefined: true,
   109  		Abort:      abort,
   110  	})
   111  
   112  	if err == nil {
   113  		// Execute build script (user commands)
   114  		err = executor.Run(ExecutorCommand{
   115  			Script: buildScript.BuildScript,
   116  			Abort:  abort,
   117  		})
   118  
   119  		// Execute after script (user commands)
   120  		if buildScript.AfterScript != "" {
   121  			timeoutCh := make(chan interface{})
   122  			go func() {
   123  				timeoutCh <- <-time.After(time.Minute * 5)
   124  			}()
   125  			executor.Run(ExecutorCommand{
   126  				Script: buildScript.AfterScript,
   127  				Abort:  timeoutCh,
   128  			})
   129  		}
   130  	}
   131  
   132  	// Execute post script (cache store, artifacts upload)
   133  	if err == nil {
   134  		err = executor.Run(ExecutorCommand{
   135  			Script:     buildScript.PostScript,
   136  			Predefined: true,
   137  			Abort:      abort,
   138  		})
   139  	}
   140  
   141  	return err
   142  }
   143  
   144  func (b *Build) run(executor Executor) (err error) {
   145  	buildTimeout := b.Timeout
   146  	if buildTimeout <= 0 {
   147  		buildTimeout = DefaultTimeout
   148  	}
   149  
   150  	buildCanceled := make(chan bool)
   151  	buildFinish := make(chan error)
   152  	buildAbort := make(chan interface{})
   153  
   154  	// Wait for cancel notification
   155  	b.Trace.Notify(func() {
   156  		buildCanceled <- true
   157  	})
   158  
   159  	// Run build script
   160  	go func() {
   161  		buildFinish <- b.executeScript(executor.ShellScript(), executor, buildAbort)
   162  	}()
   163  
   164  	// Wait for signals: cancel, timeout, abort or finish
   165  	b.log().Debugln("Waiting for signals...")
   166  	select {
   167  	case <-buildCanceled:
   168  		err = errors.New("canceled")
   169  
   170  	case <-time.After(time.Duration(buildTimeout) * time.Second):
   171  		err = fmt.Errorf("execution took longer than %v seconds", buildTimeout)
   172  
   173  	case signal := <-b.BuildAbort:
   174  		err = fmt.Errorf("aborted: %v", signal)
   175  
   176  	case err = <-buildFinish:
   177  		return err
   178  	}
   179  
   180  	b.log().Debugln("Waiting for build to finish...", err)
   181  
   182  	// Wait till we receive that build did finish
   183  	for {
   184  		select {
   185  		case buildAbort <- true:
   186  		case <-buildFinish:
   187  			return err
   188  		}
   189  	}
   190  }
   191  
   192  func (b *Build) Run(globalConfig *Config, trace BuildTrace) (err error) {
   193  	defer func() {
   194  		if err != nil {
   195  			trace.Fail(err)
   196  		} else {
   197  			trace.Success()
   198  		}
   199  	}()
   200  	b.Trace = trace
   201  
   202  	executor := NewExecutor(b.Runner.Executor)
   203  	if executor == nil {
   204  		fmt.Fprint(trace, "Executor not found:", b.Runner.Executor)
   205  		return errors.New("executor not found")
   206  	}
   207  	defer executor.Cleanup()
   208  
   209  	err = executor.Prepare(globalConfig, b.Runner, b)
   210  	if err == nil {
   211  		err = b.run(executor)
   212  	}
   213  	executor.Finish(err)
   214  	return err
   215  }
   216  
   217  func (b *Build) String() string {
   218  	return helpers.ToYAML(b)
   219  }
   220  
   221  func (b *Build) GetDefaultVariables() BuildVariables {
   222  	return BuildVariables{
   223  		{"CI", "true", true, true, false},
   224  		{"CI_BUILD_REF", b.Sha, true, true, false},
   225  		{"CI_BUILD_BEFORE_SHA", b.BeforeSha, true, true, false},
   226  		{"CI_BUILD_REF_NAME", b.RefName, true, true, false},
   227  		{"CI_BUILD_ID", strconv.Itoa(b.ID), true, true, false},
   228  		{"CI_BUILD_REPO", b.RepoURL, true, true, false},
   229  		{"CI_BUILD_TOKEN", b.Token, true, true, false},
   230  		{"CI_PROJECT_ID", strconv.Itoa(b.ProjectID), true, true, false},
   231  		{"CI_PROJECT_DIR", b.FullProjectDir(), true, true, false},
   232  		{"CI_SERVER", "yes", true, true, false},
   233  		{"CI_SERVER_NAME", "GitLab CI", true, true, false},
   234  		{"CI_SERVER_VERSION", "", true, true, false},
   235  		{"CI_SERVER_REVISION", "", true, true, false},
   236  		{"GITLAB_CI", "true", true, true, false},
   237  	}
   238  }
   239  
   240  func (b *Build) GetAllVariables() BuildVariables {
   241  	variables := b.Runner.GetVariables()
   242  	variables = append(variables, b.GetDefaultVariables()...)
   243  	variables = append(variables, b.Variables...)
   244  	return variables.Expand()
   245  }