github.com/secure-build/gitlab-runner@v12.5.0+incompatible/executors/custom/executor.go (about)

     1  package custom
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"path/filepath"
    11  
    12  	"github.com/sirupsen/logrus"
    13  
    14  	"gitlab.com/gitlab-org/gitlab-runner/common"
    15  	"gitlab.com/gitlab-org/gitlab-runner/executors"
    16  	"gitlab.com/gitlab-org/gitlab-runner/executors/custom/api"
    17  	"gitlab.com/gitlab-org/gitlab-runner/executors/custom/command"
    18  )
    19  
    20  type commandOutputs struct {
    21  	stdout io.Writer
    22  	stderr io.Writer
    23  }
    24  
    25  type prepareCommandOpts struct {
    26  	executable string
    27  	args       []string
    28  	out        commandOutputs
    29  }
    30  
    31  type ConfigExecOutput struct {
    32  	api.ConfigExecOutput
    33  }
    34  
    35  func (c *ConfigExecOutput) InjectInto(executor *executor) {
    36  	if c.Hostname != nil {
    37  		executor.Build.Hostname = *c.Hostname
    38  	}
    39  
    40  	if c.BuildsDir != nil {
    41  		executor.Config.BuildsDir = *c.BuildsDir
    42  	}
    43  
    44  	if c.CacheDir != nil {
    45  		executor.Config.CacheDir = *c.CacheDir
    46  	}
    47  
    48  	if c.BuildsDirIsShared != nil {
    49  		executor.SharedBuildsDir = *c.BuildsDirIsShared
    50  	}
    51  
    52  	executor.driverInfo = c.Driver
    53  }
    54  
    55  type executor struct {
    56  	executors.AbstractExecutor
    57  
    58  	config  *config
    59  	tempDir string
    60  
    61  	driverInfo *api.DriverInfo
    62  }
    63  
    64  func (e *executor) Prepare(options common.ExecutorPrepareOptions) error {
    65  	e.AbstractExecutor.PrepareConfiguration(options)
    66  
    67  	err := e.prepareConfig()
    68  	if err != nil {
    69  		return err
    70  	}
    71  
    72  	e.tempDir, err = ioutil.TempDir("", "custom-executor")
    73  	if err != nil {
    74  		return err
    75  	}
    76  
    77  	err = e.dynamicConfig()
    78  	if err != nil {
    79  		return err
    80  	}
    81  
    82  	e.logStartupMessage()
    83  
    84  	err = e.AbstractExecutor.PrepareBuildAndShell()
    85  	if err != nil {
    86  		return err
    87  	}
    88  
    89  	// nothing to do, as there's no prepare_script
    90  	if e.config.PrepareExec == "" {
    91  		return nil
    92  	}
    93  
    94  	ctx, cancelFunc := context.WithTimeout(e.Context, e.config.GetPrepareExecTimeout())
    95  	defer cancelFunc()
    96  
    97  	opts := prepareCommandOpts{
    98  		executable: e.config.PrepareExec,
    99  		args:       e.config.PrepareArgs,
   100  		out:        e.defaultCommandOutputs(),
   101  	}
   102  
   103  	return e.prepareCommand(ctx, opts).Run()
   104  }
   105  
   106  func (e *executor) prepareConfig() error {
   107  	if e.Config.Custom == nil {
   108  		return common.MakeBuildError("custom executor not configured")
   109  	}
   110  
   111  	e.config = &config{
   112  		CustomConfig: e.Config.Custom,
   113  	}
   114  
   115  	if e.config.RunExec == "" {
   116  		return common.MakeBuildError("custom executor is missing RunExec")
   117  	}
   118  
   119  	return nil
   120  }
   121  
   122  func (e *executor) dynamicConfig() error {
   123  	if e.config.ConfigExec == "" {
   124  		return nil
   125  	}
   126  
   127  	ctx, cancelFunc := context.WithTimeout(e.Context, e.config.GetConfigExecTimeout())
   128  	defer cancelFunc()
   129  
   130  	buf := bytes.NewBuffer(nil)
   131  	outputs := commandOutputs{
   132  		stdout: buf,
   133  		stderr: e.Trace,
   134  	}
   135  
   136  	opts := prepareCommandOpts{
   137  		executable: e.config.ConfigExec,
   138  		args:       e.config.ConfigArgs,
   139  		out:        outputs,
   140  	}
   141  
   142  	err := e.prepareCommand(ctx, opts).Run()
   143  	if err != nil {
   144  		return err
   145  	}
   146  
   147  	jsonConfig := buf.Bytes()
   148  	if len(jsonConfig) < 1 {
   149  		return nil
   150  	}
   151  
   152  	config := new(ConfigExecOutput)
   153  
   154  	err = json.Unmarshal(jsonConfig, config)
   155  	if err != nil {
   156  		return fmt.Errorf("error while parsing JSON output: %v", err)
   157  	}
   158  
   159  	config.InjectInto(e)
   160  
   161  	return nil
   162  }
   163  
   164  func (e *executor) logStartupMessage() {
   165  	const usageLine = "Using Custom executor"
   166  
   167  	info := e.driverInfo
   168  	if info == nil || info.Name == nil {
   169  		e.Println(fmt.Sprintf("%s...", usageLine))
   170  		return
   171  	}
   172  
   173  	if info.Version == nil {
   174  		e.Println(fmt.Sprintf("%s with driver %s...", usageLine, *info.Name))
   175  		return
   176  	}
   177  
   178  	e.Println(fmt.Sprintf("%s with driver %s %s...", usageLine, *info.Name, *info.Version))
   179  }
   180  
   181  func (e *executor) defaultCommandOutputs() commandOutputs {
   182  	return commandOutputs{
   183  		stdout: e.Trace,
   184  		stderr: e.Trace,
   185  	}
   186  }
   187  
   188  var commandFactory = command.New
   189  
   190  func (e *executor) prepareCommand(ctx context.Context, opts prepareCommandOpts) command.Command {
   191  	cmdOpts := command.CreateOptions{
   192  		Dir:                 e.tempDir,
   193  		Env:                 make([]string, 0),
   194  		Stdout:              opts.out.stdout,
   195  		Stderr:              opts.out.stderr,
   196  		Logger:              e.BuildLogger,
   197  		GracefulKillTimeout: e.config.GetGracefulKillTimeout(),
   198  		ForceKillTimeout:    e.config.GetForceKillTimeout(),
   199  	}
   200  
   201  	for _, variable := range e.Build.GetAllVariables() {
   202  		cmdOpts.Env = append(cmdOpts.Env, fmt.Sprintf("CUSTOM_ENV_%s=%s", variable.Key, variable.Value))
   203  	}
   204  
   205  	return commandFactory(ctx, opts.executable, opts.args, cmdOpts)
   206  }
   207  
   208  func (e *executor) Run(cmd common.ExecutorCommand) error {
   209  	scriptDir, err := ioutil.TempDir(e.tempDir, "script")
   210  	if err != nil {
   211  		return err
   212  	}
   213  
   214  	scriptFile := filepath.Join(scriptDir, "script."+e.BuildShell.Extension)
   215  	err = ioutil.WriteFile(scriptFile, []byte(cmd.Script), 0700)
   216  	if err != nil {
   217  		return err
   218  	}
   219  
   220  	args := append(e.config.RunArgs, scriptFile, string(cmd.Stage))
   221  
   222  	opts := prepareCommandOpts{
   223  		executable: e.config.RunExec,
   224  		args:       args,
   225  		out:        e.defaultCommandOutputs(),
   226  	}
   227  
   228  	return e.prepareCommand(cmd.Context, opts).Run()
   229  }
   230  
   231  func (e *executor) Cleanup() {
   232  	e.AbstractExecutor.Cleanup()
   233  
   234  	err := e.prepareConfig()
   235  	if err != nil {
   236  		e.Warningln(err)
   237  
   238  		// at this moment we don't care about the errors
   239  		return
   240  	}
   241  
   242  	// nothing to do, as there's no cleanup_script
   243  	if e.config.CleanupExec == "" {
   244  		return
   245  	}
   246  
   247  	ctx, cancelFunc := context.WithTimeout(context.Background(), e.config.GetCleanupScriptTimeout())
   248  	defer cancelFunc()
   249  
   250  	stdoutLogger := e.BuildLogger.WithFields(logrus.Fields{"cleanup_std": "out"})
   251  	stderrLogger := e.BuildLogger.WithFields(logrus.Fields{"cleanup_std": "err"})
   252  
   253  	outputs := commandOutputs{
   254  		stdout: stdoutLogger.WriterLevel(logrus.DebugLevel),
   255  		stderr: stderrLogger.WriterLevel(logrus.WarnLevel),
   256  	}
   257  
   258  	opts := prepareCommandOpts{
   259  		executable: e.config.CleanupExec,
   260  		args:       e.config.CleanupArgs,
   261  		out:        outputs,
   262  	}
   263  
   264  	err = e.prepareCommand(ctx, opts).Run()
   265  	if err != nil {
   266  		e.Warningln("Cleanup script failed:", err)
   267  	}
   268  }
   269  
   270  func init() {
   271  	options := executors.ExecutorOptions{
   272  		DefaultCustomBuildsDirEnabled: false,
   273  		Shell: common.ShellScriptInfo{
   274  			Shell:         common.GetDefaultShell(),
   275  			Type:          common.NormalShell,
   276  			RunnerCommand: "gitlab-runner",
   277  		},
   278  		ShowHostname: false,
   279  	}
   280  
   281  	creator := func() common.Executor {
   282  		return &executor{
   283  			AbstractExecutor: executors.AbstractExecutor{
   284  				ExecutorOptions: options,
   285  			},
   286  		}
   287  	}
   288  
   289  	featuresUpdater := func(features *common.FeaturesInfo) {
   290  		features.Variables = true
   291  		features.Shared = true
   292  	}
   293  
   294  	common.RegisterExecutor("custom", executors.DefaultExecutorProvider{
   295  		Creator:          creator,
   296  		FeaturesUpdater:  featuresUpdater,
   297  		DefaultShellName: options.Shell.Shell,
   298  	})
   299  }