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

     1  package custom
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path/filepath"
    10  	"runtime"
    11  	"testing"
    12  
    13  	"github.com/pkg/errors"
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/mock"
    16  	"github.com/stretchr/testify/require"
    17  
    18  	"gitlab.com/gitlab-org/gitlab-runner/common"
    19  	"gitlab.com/gitlab-org/gitlab-runner/executors/custom/command"
    20  )
    21  
    22  type executorTestCase struct {
    23  	config common.RunnerConfig
    24  
    25  	commandStdoutContent string
    26  	commandStderrContent string
    27  	commandErr           error
    28  
    29  	doNotMockCommandFactory bool
    30  
    31  	adjustExecutor func(t *testing.T, e *executor)
    32  
    33  	assertBuild          func(t *testing.T, b *common.Build)
    34  	assertCommandFactory func(t *testing.T, tt executorTestCase, ctx context.Context, executable string, args []string, options command.CreateOptions)
    35  	assertOutput         func(t *testing.T, output string)
    36  	expectedError        string
    37  }
    38  
    39  func getRunnerConfig(custom *common.CustomConfig) common.RunnerConfig {
    40  	rc := common.RunnerConfig{
    41  		RunnerCredentials: common.RunnerCredentials{
    42  			Token: "RuNnErToKeN",
    43  		},
    44  		RunnerSettings: common.RunnerSettings{
    45  			BuildsDir: "/builds",
    46  			CacheDir:  "/cache",
    47  			Shell:     "bash",
    48  		},
    49  	}
    50  
    51  	if custom != nil {
    52  		rc.Custom = custom
    53  	}
    54  
    55  	return rc
    56  }
    57  
    58  func prepareExecutorForCleanup(t *testing.T, tt executorTestCase) (*executor, *bytes.Buffer) {
    59  	e, options, out := prepareExecutor(t, tt)
    60  
    61  	e.Config = *options.Config
    62  	e.Build = options.Build
    63  	e.Trace = options.Trace
    64  	e.BuildLogger = common.NewBuildLogger(e.Trace, e.Build.Log())
    65  
    66  	return e, out
    67  }
    68  
    69  func prepareExecutor(t *testing.T, tt executorTestCase) (*executor, common.ExecutorPrepareOptions, *bytes.Buffer) {
    70  	out := bytes.NewBuffer([]byte{})
    71  
    72  	successfulBuild, err := common.GetSuccessfulBuild()
    73  	require.NoError(t, err)
    74  
    75  	successfulBuild.ID = jobID()
    76  
    77  	trace := new(common.MockJobTrace)
    78  	defer trace.AssertExpectations(t)
    79  
    80  	trace.On("Write", mock.Anything).
    81  		Run(func(args mock.Arguments) {
    82  			_, err := io.Copy(out, bytes.NewReader(args.Get(0).([]byte)))
    83  			require.NoError(t, err)
    84  		}).
    85  		Return(0, nil).
    86  		Maybe()
    87  	trace.On("IsStdout").
    88  		Return(false).
    89  		Maybe()
    90  
    91  	options := common.ExecutorPrepareOptions{
    92  		Build: &common.Build{
    93  			JobResponse: successfulBuild,
    94  			Runner:      &tt.config,
    95  		},
    96  		Config:  &tt.config,
    97  		Context: context.Background(),
    98  		Trace:   trace,
    99  	}
   100  
   101  	e := new(executor)
   102  
   103  	return e, options, out
   104  }
   105  
   106  var currentJobID = 0
   107  
   108  func jobID() int {
   109  	i := currentJobID
   110  	currentJobID++
   111  
   112  	return i
   113  }
   114  
   115  func assertOutput(t *testing.T, tt executorTestCase, out *bytes.Buffer) {
   116  	if tt.assertOutput == nil {
   117  		return
   118  	}
   119  
   120  	tt.assertOutput(t, out.String())
   121  }
   122  
   123  func mockCommandFactory(t *testing.T, tt executorTestCase) func() {
   124  	if tt.doNotMockCommandFactory {
   125  		return func() {}
   126  	}
   127  
   128  	outputs := commandOutputs{
   129  		stdout: nil,
   130  		stderr: nil,
   131  	}
   132  
   133  	cmd := new(command.MockCommand)
   134  	cmd.On("Run").
   135  		Run(func(_ mock.Arguments) {
   136  			if tt.commandStdoutContent != "" && outputs.stdout != nil {
   137  				_, err := fmt.Fprintln(outputs.stdout, tt.commandStdoutContent)
   138  				require.NoError(t, err, "Unexpected error on mocking command output to stdout")
   139  			}
   140  
   141  			if tt.commandStderrContent != "" && outputs.stderr != nil {
   142  				_, err := fmt.Fprintln(outputs.stderr, tt.commandStderrContent)
   143  				require.NoError(t, err, "Unexpected error on mocking command output to stderr")
   144  			}
   145  		}).
   146  		Return(tt.commandErr)
   147  
   148  	oldFactory := commandFactory
   149  	commandFactory = func(ctx context.Context, executable string, args []string, options command.CreateOptions) command.Command {
   150  		if tt.assertCommandFactory != nil {
   151  			tt.assertCommandFactory(t, tt, ctx, executable, args, options)
   152  		}
   153  
   154  		outputs.stdout = options.Stdout
   155  		outputs.stderr = options.Stderr
   156  
   157  		return cmd
   158  	}
   159  
   160  	return func() {
   161  		cmd.AssertExpectations(t)
   162  		commandFactory = oldFactory
   163  	}
   164  }
   165  
   166  func TestExecutor_Prepare(t *testing.T) {
   167  	tests := map[string]executorTestCase{
   168  		"AbstractExecutor.Prepare failure": {
   169  			config:                  common.RunnerConfig{},
   170  			doNotMockCommandFactory: true,
   171  			expectedError:           "custom executor not configured",
   172  		},
   173  		"custom executor not set": {
   174  			config:                  getRunnerConfig(nil),
   175  			doNotMockCommandFactory: true,
   176  			expectedError:           "custom executor not configured",
   177  		},
   178  		"custom executor set without RunExec": {
   179  			config:                  getRunnerConfig(&common.CustomConfig{}),
   180  			doNotMockCommandFactory: true,
   181  			expectedError:           "custom executor is missing RunExec",
   182  		},
   183  		"custom executor set": {
   184  			config: getRunnerConfig(&common.CustomConfig{
   185  				RunExec: "bash",
   186  			}),
   187  			doNotMockCommandFactory: true,
   188  			assertOutput: func(t *testing.T, output string) {
   189  				assert.Contains(t, output, "Using Custom executor...")
   190  			},
   191  		},
   192  		"custom executor set with ConfigExec with error": {
   193  			config: getRunnerConfig(&common.CustomConfig{
   194  				RunExec:    "bash",
   195  				ConfigExec: "echo",
   196  				ConfigArgs: []string{"test"},
   197  			}),
   198  			commandErr: errors.New("test-error"),
   199  			assertCommandFactory: func(t *testing.T, tt executorTestCase, ctx context.Context, executable string, args []string, options command.CreateOptions) {
   200  				assert.Equal(t, tt.config.Custom.ConfigExec, executable)
   201  				assert.Equal(t, tt.config.Custom.ConfigArgs, args)
   202  			},
   203  			assertOutput: func(t *testing.T, output string) {
   204  				assert.NotContains(t, output, "Using Custom executor...")
   205  			},
   206  			expectedError: "test-error",
   207  		},
   208  		"custom executor set with ConfigExec with invalid JSON": {
   209  			config: getRunnerConfig(&common.CustomConfig{
   210  				RunExec:    "bash",
   211  				ConfigExec: "echo",
   212  			}),
   213  			commandStdoutContent: "abcd",
   214  			commandErr:           nil,
   215  			assertCommandFactory: func(t *testing.T, tt executorTestCase, ctx context.Context, executable string, args []string, options command.CreateOptions) {
   216  				assert.Equal(t, tt.config.Custom.ConfigExec, executable)
   217  			},
   218  			assertOutput: func(t *testing.T, output string) {
   219  				assert.NotContains(t, output, "Using Custom executor...")
   220  			},
   221  			expectedError: "error while parsing JSON output: invalid character 'a' looking for beginning of value",
   222  		},
   223  		"custom executor set with ConfigExec with empty JSON": {
   224  			config: getRunnerConfig(&common.CustomConfig{
   225  				RunExec:    "bash",
   226  				ConfigExec: "echo",
   227  			}),
   228  			commandStdoutContent: "",
   229  			commandErr:           nil,
   230  			assertCommandFactory: func(t *testing.T, tt executorTestCase, ctx context.Context, executable string, args []string, options command.CreateOptions) {
   231  				assert.Equal(t, tt.config.Custom.ConfigExec, executable)
   232  			},
   233  			assertOutput: func(t *testing.T, output string) {
   234  				assert.Contains(t, output, "Using Custom executor...")
   235  			},
   236  			assertBuild: func(t *testing.T, b *common.Build) {
   237  				assert.Equal(t, "/builds/project-0", b.BuildDir)
   238  				assert.Equal(t, "/cache/project-0", b.CacheDir)
   239  			},
   240  		},
   241  		"custom executor set with ConfigExec with undefined builds_dir": {
   242  			config: getRunnerConfig(&common.CustomConfig{
   243  				RunExec:    "bash",
   244  				ConfigExec: "echo",
   245  			}),
   246  			commandStdoutContent: `{"builds_dir":""}`,
   247  			commandErr:           nil,
   248  			assertCommandFactory: func(t *testing.T, tt executorTestCase, ctx context.Context, executable string, args []string, options command.CreateOptions) {
   249  				assert.Equal(t, tt.config.Custom.ConfigExec, executable)
   250  			},
   251  			assertOutput: func(t *testing.T, output string) {
   252  				assert.Contains(t, output, "Using Custom executor...")
   253  			},
   254  			expectedError: "the builds_dir is not configured",
   255  		},
   256  		"custom executor set with ConfigExec and driver info missing name": {
   257  			config: getRunnerConfig(&common.CustomConfig{
   258  				RunExec:    "bash",
   259  				ConfigExec: "echo",
   260  			}),
   261  			commandStdoutContent: `{
   262  				"driver": {
   263  					"version": "v0.0.1"
   264  				}
   265  			}`,
   266  			commandErr: nil,
   267  			assertCommandFactory: func(t *testing.T, tt executorTestCase, ctx context.Context, executable string, args []string, options command.CreateOptions) {
   268  				assert.Equal(t, tt.config.Custom.ConfigExec, executable)
   269  			},
   270  			assertOutput: func(t *testing.T, output string) {
   271  				assert.Contains(t, output, "Using Custom executor...")
   272  			},
   273  		},
   274  		"custom executor set with ConfigExec and driver info missing version": {
   275  			config: getRunnerConfig(&common.CustomConfig{
   276  				RunExec:    "bash",
   277  				ConfigExec: "echo",
   278  			}),
   279  			commandStdoutContent: `{
   280  				"driver": {
   281  					"name": "test driver"
   282  				}
   283  			}`,
   284  			commandErr: nil,
   285  			assertCommandFactory: func(t *testing.T, tt executorTestCase, ctx context.Context, executable string, args []string, options command.CreateOptions) {
   286  				assert.Equal(t, tt.config.Custom.ConfigExec, executable)
   287  			},
   288  			assertOutput: func(t *testing.T, output string) {
   289  				assert.Contains(t, output, "Using Custom executor with driver test driver...")
   290  			},
   291  		},
   292  		"custom executor set with ConfigExec": {
   293  			config: getRunnerConfig(&common.CustomConfig{
   294  				RunExec:    "bash",
   295  				ConfigExec: "echo",
   296  			}),
   297  			commandStdoutContent: `{
   298  				"hostname": "custom-hostname",
   299  				"builds_dir": "/some/build/directory",
   300  				"cache_dir": "/some/cache/directory",
   301  				"builds_dir_is_shared":true,
   302  				"driver": {
   303  					"name": "test driver",
   304  					"version": "v0.0.1"
   305  				}
   306  			}`,
   307  			commandErr: nil,
   308  			assertCommandFactory: func(t *testing.T, tt executorTestCase, ctx context.Context, executable string, args []string, options command.CreateOptions) {
   309  				assert.Equal(t, tt.config.Custom.ConfigExec, executable)
   310  			},
   311  			assertOutput: func(t *testing.T, output string) {
   312  				assert.Contains(t, output, "Using Custom executor with driver test driver v0.0.1...")
   313  			},
   314  			assertBuild: func(t *testing.T, b *common.Build) {
   315  				assert.Equal(t, "custom-hostname", b.Hostname)
   316  				assert.Equal(t, "/some/build/directory/RuNnErTo/0/project-0", b.BuildDir)
   317  				assert.Equal(t, "/some/cache/directory/project-0", b.CacheDir)
   318  			},
   319  		},
   320  		"custom executor set with PrepareExec": {
   321  			config: getRunnerConfig(&common.CustomConfig{
   322  				RunExec:     "bash",
   323  				PrepareExec: "echo",
   324  				PrepareArgs: []string{"test"},
   325  			}),
   326  			assertCommandFactory: func(t *testing.T, tt executorTestCase, ctx context.Context, executable string, args []string, options command.CreateOptions) {
   327  				assert.Equal(t, tt.config.Custom.PrepareExec, executable)
   328  				assert.Equal(t, tt.config.Custom.PrepareArgs, args)
   329  			},
   330  			assertOutput: func(t *testing.T, output string) {
   331  				assert.Contains(t, output, "Using Custom executor...")
   332  			},
   333  		},
   334  		"custom executor set with PrepareExec with error": {
   335  			config: getRunnerConfig(&common.CustomConfig{
   336  				RunExec:     "bash",
   337  				PrepareExec: "echo",
   338  				PrepareArgs: []string{"test"},
   339  			}),
   340  			commandErr: errors.New("test-error"),
   341  			assertCommandFactory: func(t *testing.T, tt executorTestCase, ctx context.Context, executable string, args []string, options command.CreateOptions) {
   342  				assert.Equal(t, tt.config.Custom.PrepareExec, executable)
   343  				assert.Equal(t, tt.config.Custom.PrepareArgs, args)
   344  			},
   345  			assertOutput: func(t *testing.T, output string) {
   346  				assert.Contains(t, output, "Using Custom executor...")
   347  			},
   348  			expectedError: "test-error",
   349  		},
   350  	}
   351  
   352  	for testName, tt := range tests {
   353  		t.Run(testName, func(t *testing.T) {
   354  			defer mockCommandFactory(t, tt)()
   355  
   356  			e, options, out := prepareExecutor(t, tt)
   357  			err := e.Prepare(options)
   358  
   359  			assertOutput(t, tt, out)
   360  
   361  			if tt.assertBuild != nil {
   362  				tt.assertBuild(t, e.Build)
   363  			}
   364  
   365  			if tt.expectedError == "" {
   366  				assert.NoError(t, err)
   367  
   368  				return
   369  			}
   370  
   371  			assert.EqualError(t, err, tt.expectedError)
   372  		})
   373  	}
   374  }
   375  
   376  func TestExecutor_Cleanup(t *testing.T) {
   377  	tests := map[string]executorTestCase{
   378  		"custom executor not set": {
   379  			config: getRunnerConfig(nil),
   380  			assertOutput: func(t *testing.T, output string) {
   381  				assert.Contains(t, output, "custom executor not configured")
   382  			},
   383  			doNotMockCommandFactory: true,
   384  		},
   385  		"custom executor set without RunExec": {
   386  			config: getRunnerConfig(&common.CustomConfig{}),
   387  			assertOutput: func(t *testing.T, output string) {
   388  				assert.Contains(t, output, "custom executor is missing RunExec")
   389  			},
   390  			doNotMockCommandFactory: true,
   391  		},
   392  		"custom executor set": {
   393  			config: getRunnerConfig(&common.CustomConfig{
   394  				RunExec: "bash",
   395  			}),
   396  			doNotMockCommandFactory: true,
   397  		},
   398  		"custom executor set with CleanupExec": {
   399  			config: getRunnerConfig(&common.CustomConfig{
   400  				RunExec:     "bash",
   401  				CleanupExec: "echo",
   402  				CleanupArgs: []string{"test"},
   403  			}),
   404  			assertCommandFactory: func(t *testing.T, tt executorTestCase, ctx context.Context, executable string, args []string, options command.CreateOptions) {
   405  				assert.Equal(t, tt.config.Custom.CleanupExec, executable)
   406  				assert.Equal(t, tt.config.Custom.CleanupArgs, args)
   407  			},
   408  			assertOutput: func(t *testing.T, output string) {
   409  				assert.NotContains(t, output, "WARNING: Cleanup script failed:")
   410  			},
   411  		},
   412  		"custom executor set with CleanupExec with error": {
   413  			config: getRunnerConfig(&common.CustomConfig{
   414  				RunExec:     "bash",
   415  				CleanupExec: "unknown",
   416  			}),
   417  			commandStdoutContent: "some output message in commands output",
   418  			commandStderrContent: "some error message in commands output",
   419  			commandErr:           errors.New("test-error"),
   420  			assertCommandFactory: func(t *testing.T, tt executorTestCase, ctx context.Context, executable string, args []string, options command.CreateOptions) {
   421  				assert.Equal(t, tt.config.Custom.CleanupExec, executable)
   422  			},
   423  			assertOutput: func(t *testing.T, output string) {
   424  				assert.Contains(t, output, "WARNING: Cleanup script failed: test-error")
   425  			},
   426  		},
   427  	}
   428  
   429  	for testName, tt := range tests {
   430  		t.Run(testName, func(t *testing.T) {
   431  			defer mockCommandFactory(t, tt)()
   432  
   433  			e, out := prepareExecutorForCleanup(t, tt)
   434  			e.Cleanup()
   435  
   436  			assertOutput(t, tt, out)
   437  		})
   438  	}
   439  }
   440  
   441  func TestExecutor_Run(t *testing.T) {
   442  	tests := map[string]executorTestCase{
   443  		"Run fails on tempdir operations": {
   444  			config: getRunnerConfig(&common.CustomConfig{
   445  				RunExec: "bash",
   446  			}),
   447  			doNotMockCommandFactory: true,
   448  			adjustExecutor: func(t *testing.T, e *executor) {
   449  				curDir, err := os.Getwd()
   450  				require.NoError(t, err)
   451  				e.tempDir = filepath.Join(curDir, "unknown")
   452  			},
   453  			expectedError: func() string {
   454  				if runtime.GOOS == "windows" {
   455  					return "The system cannot find the file specified"
   456  				}
   457  
   458  				return "no such file or directory"
   459  			}(),
   460  		},
   461  		"Run executes job": {
   462  			config: getRunnerConfig(&common.CustomConfig{
   463  				RunExec: "bash",
   464  			}),
   465  			assertCommandFactory: func(t *testing.T, tt executorTestCase, ctx context.Context, executable string, args []string, options command.CreateOptions) {
   466  				assert.Equal(t, tt.config.Custom.RunExec, executable)
   467  			},
   468  		},
   469  		"Run executes job with error": {
   470  			config: getRunnerConfig(&common.CustomConfig{
   471  				RunExec:     "bash",
   472  				CleanupExec: "unknown",
   473  			}),
   474  			commandErr: errors.New("test-error"),
   475  			assertCommandFactory: func(t *testing.T, tt executorTestCase, ctx context.Context, executable string, args []string, options command.CreateOptions) {
   476  				assert.Equal(t, tt.config.Custom.RunExec, executable)
   477  			},
   478  			expectedError: "test-error",
   479  		},
   480  	}
   481  
   482  	for testName, tt := range tests {
   483  		t.Run(testName, func(t *testing.T) {
   484  			defer mockCommandFactory(t, tt)()
   485  
   486  			e, options, out := prepareExecutor(t, tt)
   487  
   488  			err := e.Prepare(options)
   489  			require.NoError(t, err)
   490  
   491  			if tt.adjustExecutor != nil {
   492  				tt.adjustExecutor(t, e)
   493  			}
   494  
   495  			err = e.Run(common.ExecutorCommand{
   496  				Context: context.Background(),
   497  			})
   498  
   499  			assertOutput(t, tt, out)
   500  
   501  			if tt.expectedError == "" {
   502  				assert.NoError(t, err)
   503  
   504  				return
   505  			}
   506  
   507  			require.Error(t, err)
   508  			assert.Contains(t, err.Error(), tt.expectedError)
   509  		})
   510  	}
   511  }