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

     1  package custom_test
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"runtime"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/require"
    16  
    17  	"gitlab.com/gitlab-org/gitlab-runner/common"
    18  	"gitlab.com/gitlab-org/gitlab-runner/executors/custom/command"
    19  	"gitlab.com/gitlab-org/gitlab-runner/helpers"
    20  	"gitlab.com/gitlab-org/gitlab-runner/session"
    21  	"gitlab.com/gitlab-org/gitlab-runner/shells/shellstest"
    22  )
    23  
    24  const (
    25  	TestTimeout = 60 * time.Second
    26  )
    27  
    28  var testExecutorFile string
    29  
    30  func TestMain(m *testing.M) {
    31  	fmt.Println("Compiling test executor")
    32  
    33  	curDir, err := os.Getwd()
    34  	if err != nil {
    35  		panic(fmt.Sprintf("Error on getting the working directory"))
    36  	}
    37  
    38  	sourcesDir := filepath.Join(curDir, "testdata", "test_executor")
    39  	sourcesFile := filepath.Join(sourcesDir, "main.go")
    40  
    41  	targetDir, err := ioutil.TempDir("", "test_executor")
    42  	if err != nil {
    43  		panic(fmt.Sprintf("Error on preparing tmp directory for test executor binary"))
    44  	}
    45  	testExecutorFile = filepath.Join(targetDir, "main")
    46  
    47  	if runtime.GOOS == "windows" {
    48  		// Adding it here, explicitly, in if, to show that this OS
    49  		// requires a special treatment...
    50  		testExecutorFile += ".exe"
    51  	}
    52  
    53  	cmd := exec.Command("go", "build", "-o", testExecutorFile, sourcesFile)
    54  	cmd.Stdout = os.Stdout
    55  	cmd.Stderr = os.Stderr
    56  
    57  	fmt.Printf("Executing: %v", cmd)
    58  	fmt.Println()
    59  
    60  	err = cmd.Run()
    61  	if err != nil {
    62  		panic(fmt.Sprintf("Error on executing go build to prepare test custom executor"))
    63  	}
    64  
    65  	code := m.Run()
    66  	os.Exit(code)
    67  }
    68  
    69  func runBuildWithOptions(t *testing.T, build *common.Build, config *common.Config, trace *common.Trace) error {
    70  	timeoutTimer := time.AfterFunc(TestTimeout, func() {
    71  		t.Log("Timed out")
    72  		t.FailNow()
    73  	})
    74  	defer timeoutTimer.Stop()
    75  
    76  	return build.Run(config, trace)
    77  }
    78  
    79  func runBuildWithTrace(t *testing.T, build *common.Build, trace *common.Trace) error {
    80  	return runBuildWithOptions(t, build, &common.Config{}, trace)
    81  }
    82  
    83  func runBuild(t *testing.T, build *common.Build) error {
    84  	err := runBuildWithTrace(t, build, &common.Trace{Writer: os.Stdout})
    85  	assert.True(t, build.IsSharedEnv())
    86  
    87  	return err
    88  }
    89  
    90  func runBuildReturningOutput(t *testing.T, build *common.Build) (string, error) {
    91  	buf := bytes.NewBuffer(nil)
    92  	err := runBuildWithTrace(t, build, &common.Trace{Writer: buf})
    93  	output := buf.String()
    94  	t.Log(output)
    95  
    96  	return output, err
    97  }
    98  
    99  func newBuild(t *testing.T, jobResponse common.JobResponse, shell string) (*common.Build, func()) {
   100  	dir, err := ioutil.TempDir("", "gitlab-runner-custom-executor-test")
   101  	require.NoError(t, err)
   102  
   103  	t.Log("Build directory:", dir)
   104  
   105  	build := &common.Build{
   106  		JobResponse: jobResponse,
   107  		Runner: &common.RunnerConfig{
   108  			RunnerSettings: common.RunnerSettings{
   109  				BuildsDir: filepath.Join(dir, "builds"),
   110  				CacheDir:  filepath.Join(dir, "cache"),
   111  				Executor:  "custom",
   112  				Shell:     shell,
   113  				Custom: &common.CustomConfig{
   114  					ConfigExec:          testExecutorFile,
   115  					ConfigArgs:          []string{shell, "config"},
   116  					PrepareExec:         testExecutorFile,
   117  					PrepareArgs:         []string{shell, "prepare"},
   118  					RunExec:             testExecutorFile,
   119  					RunArgs:             []string{shell, "run"},
   120  					CleanupExec:         testExecutorFile,
   121  					CleanupArgs:         []string{shell, "cleanup"},
   122  					GracefulKillTimeout: timeoutInSeconds(10 * time.Second),
   123  					ForceKillTimeout:    timeoutInSeconds(10 * time.Second),
   124  				},
   125  			},
   126  		},
   127  		SystemInterrupt: make(chan os.Signal, 1),
   128  		Session: &session.Session{
   129  			DisconnectCh: make(chan error),
   130  			TimeoutCh:    make(chan error),
   131  		},
   132  	}
   133  
   134  	cleanup := func() {
   135  		_ = os.RemoveAll(dir)
   136  	}
   137  
   138  	return build, cleanup
   139  }
   140  
   141  func timeoutInSeconds(duration time.Duration) *int {
   142  	seconds := duration.Seconds()
   143  	secondsInInt := int(seconds)
   144  
   145  	return &secondsInInt
   146  }
   147  
   148  func TestBuildSuccess(t *testing.T) {
   149  	shellstest.OnEachShell(t, func(t *testing.T, shell string) {
   150  		successfulBuild, err := common.GetSuccessfulBuild()
   151  		require.NoError(t, err)
   152  
   153  		build, cleanup := newBuild(t, successfulBuild, shell)
   154  		defer cleanup()
   155  
   156  		err = runBuild(t, build)
   157  		assert.NoError(t, err)
   158  	})
   159  }
   160  
   161  func TestBuildBuildFailure(t *testing.T) {
   162  	shellstest.OnEachShell(t, func(t *testing.T, shell string) {
   163  		successfulBuild, err := common.GetSuccessfulBuild()
   164  		require.NoError(t, err)
   165  
   166  		build, cleanup := newBuild(t, successfulBuild, shell)
   167  		defer cleanup()
   168  
   169  		build.Variables = append(build.Variables, common.JobVariable{
   170  			Key:    "IS_BUILD_ERROR",
   171  			Value:  "true",
   172  			Public: true,
   173  		})
   174  
   175  		err = runBuild(t, build)
   176  		assert.Error(t, err)
   177  		assert.IsType(t, &common.BuildError{}, err)
   178  	})
   179  }
   180  
   181  func TestBuildSystemFailure(t *testing.T) {
   182  	shellstest.OnEachShell(t, func(t *testing.T, shell string) {
   183  		successfulBuild, err := common.GetSuccessfulBuild()
   184  		require.NoError(t, err)
   185  
   186  		build, cleanup := newBuild(t, successfulBuild, shell)
   187  		defer cleanup()
   188  
   189  		build.Variables = append(build.Variables, common.JobVariable{
   190  			Key:    "IS_SYSTEM_ERROR",
   191  			Value:  "true",
   192  			Public: true,
   193  		})
   194  
   195  		err = runBuild(t, build)
   196  		assert.Error(t, err)
   197  		assert.IsType(t, &exec.ExitError{}, err)
   198  		t.Log(err)
   199  	})
   200  }
   201  
   202  func TestBuildUnknownFailure(t *testing.T) {
   203  	shellstest.OnEachShell(t, func(t *testing.T, shell string) {
   204  		successfulBuild, err := common.GetSuccessfulBuild()
   205  		require.NoError(t, err)
   206  
   207  		build, cleanup := newBuild(t, successfulBuild, shell)
   208  		defer cleanup()
   209  
   210  		build.Variables = append(build.Variables, common.JobVariable{
   211  			Key:    "IS_UNKNOWN_ERROR",
   212  			Value:  "true",
   213  			Public: true,
   214  		})
   215  
   216  		err = runBuild(t, build)
   217  		assert.Error(t, err)
   218  		assert.IsType(t, &command.ErrUnknownFailure{}, err)
   219  	})
   220  }
   221  
   222  func TestBuildAbort(t *testing.T) {
   223  	shellstest.OnEachShell(t, func(t *testing.T, shell string) {
   224  		longRunningBuild, err := common.GetLongRunningBuild()
   225  		require.NoError(t, err)
   226  
   227  		build, cleanup := newBuild(t, longRunningBuild, shell)
   228  		defer cleanup()
   229  
   230  		abortTimer := time.AfterFunc(time.Second, func() {
   231  			t.Log("Interrupt")
   232  			build.SystemInterrupt <- os.Interrupt
   233  		})
   234  		defer abortTimer.Stop()
   235  
   236  		err = runBuild(t, build)
   237  		assert.EqualError(t, err, "aborted: interrupt")
   238  	})
   239  }
   240  
   241  func TestBuildCancel(t *testing.T) {
   242  	shellstest.OnEachShell(t, func(t *testing.T, shell string) {
   243  		longRunningBuild, err := common.GetLongRunningBuild()
   244  		require.NoError(t, err)
   245  
   246  		build, cleanup := newBuild(t, longRunningBuild, shell)
   247  		defer cleanup()
   248  
   249  		trace := &common.Trace{Writer: os.Stdout}
   250  
   251  		cancelTimer := time.AfterFunc(2*time.Second, func() {
   252  			t.Log("Cancel")
   253  			trace.CancelFunc()
   254  		})
   255  		defer cancelTimer.Stop()
   256  
   257  		err = runBuildWithTrace(t, build, trace)
   258  		assert.EqualError(t, err, "canceled")
   259  		assert.IsType(t, err, &common.BuildError{})
   260  	})
   261  }
   262  
   263  func TestBuildWithGitStrategyCloneWithoutLFS(t *testing.T) {
   264  	shellstest.OnEachShell(t, func(t *testing.T, shell string) {
   265  		successfulBuild, err := common.GetSuccessfulBuild()
   266  		require.NoError(t, err)
   267  
   268  		build, cleanup := newBuild(t, successfulBuild, shell)
   269  		defer cleanup()
   270  
   271  		build.Runner.PreCloneScript = "echo pre-clone-script"
   272  		build.Variables = append(build.Variables, common.JobVariable{Key: "GIT_STRATEGY", Value: "clone"})
   273  
   274  		out, err := runBuildReturningOutput(t, build)
   275  		assert.NoError(t, err)
   276  		assert.Contains(t, out, "Created fresh repository")
   277  
   278  		out, err = runBuildReturningOutput(t, build)
   279  		assert.NoError(t, err)
   280  		assert.Contains(t, out, "Created fresh repository")
   281  		assert.Regexp(t, "Checking out [a-f0-9]+ as", out)
   282  		assert.Contains(t, out, "pre-clone-script")
   283  	})
   284  }
   285  
   286  func TestBuildWithGitStrategyCloneNoCheckoutWithoutLFS(t *testing.T) {
   287  	shellstest.OnEachShell(t, func(t *testing.T, shell string) {
   288  		successfulBuild, err := common.GetSuccessfulBuild()
   289  		require.NoError(t, err)
   290  
   291  		build, cleanup := newBuild(t, successfulBuild, shell)
   292  		defer cleanup()
   293  
   294  		build.Runner.PreCloneScript = "echo pre-clone-script"
   295  		build.Variables = append(build.Variables, common.JobVariable{Key: "GIT_STRATEGY", Value: "clone"})
   296  		build.Variables = append(build.Variables, common.JobVariable{Key: "GIT_CHECKOUT", Value: "false"})
   297  
   298  		out, err := runBuildReturningOutput(t, build)
   299  		assert.NoError(t, err)
   300  		assert.Contains(t, out, "Created fresh repository")
   301  
   302  		out, err = runBuildReturningOutput(t, build)
   303  		assert.NoError(t, err)
   304  		assert.Contains(t, out, "Created fresh repository")
   305  		assert.Contains(t, out, "Skipping Git checkout")
   306  		assert.Contains(t, out, "pre-clone-script")
   307  	})
   308  }
   309  
   310  func TestBuildWithGitSubmoduleStrategyRecursiveAndGitStrategyNone(t *testing.T) {
   311  	shellstest.OnEachShell(t, func(t *testing.T, shell string) {
   312  		successfulBuild, err := common.GetSuccessfulBuild()
   313  		require.NoError(t, err)
   314  
   315  		build, cleanup := newBuild(t, successfulBuild, shell)
   316  		defer cleanup()
   317  
   318  		build.Variables = append(build.Variables, common.JobVariable{Key: "GIT_STRATEGY", Value: "none"})
   319  		build.Variables = append(build.Variables, common.JobVariable{Key: "GIT_SUBMODULE_STRATEGY", Value: "recursive"})
   320  
   321  		out, err := runBuildReturningOutput(t, build)
   322  		assert.NoError(t, err)
   323  		assert.NotContains(t, out, "Created fresh repository")
   324  		assert.NotContains(t, out, "Fetching changes")
   325  		assert.Contains(t, out, "Skipping Git repository setup")
   326  		assert.NotContains(t, out, "Updating/initializing submodules...")
   327  		assert.NotContains(t, out, "Updating/initializing submodules recursively...")
   328  		assert.Contains(t, out, "Skipping Git submodules setup")
   329  	})
   330  }
   331  
   332  func TestBuildWithoutDebugTrace(t *testing.T) {
   333  	shellstest.OnEachShell(t, func(t *testing.T, shell string) {
   334  		successfulBuild, err := common.GetSuccessfulBuild()
   335  		require.NoError(t, err)
   336  
   337  		build, cleanup := newBuild(t, successfulBuild, shell)
   338  		defer cleanup()
   339  
   340  		// The default build shouldn't have debug tracing enabled
   341  		out, err := runBuildReturningOutput(t, build)
   342  		assert.NoError(t, err)
   343  		assert.NotRegexp(t, `[^$] echo Hello World`, out)
   344  	})
   345  }
   346  
   347  func TestBuildWithDebugTrace(t *testing.T) {
   348  	shellstest.OnEachShell(t, func(t *testing.T, shell string) {
   349  		successfulBuild, err := common.GetSuccessfulBuild()
   350  		require.NoError(t, err)
   351  
   352  		build, cleanup := newBuild(t, successfulBuild, shell)
   353  		defer cleanup()
   354  
   355  		build.Variables = append(build.Variables, common.JobVariable{Key: "CI_DEBUG_TRACE", Value: "true"})
   356  
   357  		out, err := runBuildReturningOutput(t, build)
   358  		assert.NoError(t, err)
   359  		assert.Regexp(t, `(>|[^$] )echo Hello World`, out)
   360  	})
   361  }
   362  
   363  func TestBuildMultilineCommand(t *testing.T) {
   364  	buildGenerators := map[string]func() (common.JobResponse, error){
   365  		"bash":       common.GetMultilineBashBuild,
   366  		"powershell": common.GetMultilineBashBuildPowerShell,
   367  		"cmd":        common.GetMultilineBashBuildCmd,
   368  	}
   369  
   370  	shellstest.OnEachShell(t, func(t *testing.T, shell string) {
   371  		buildGenerator, ok := buildGenerators[shell]
   372  		require.Truef(t, ok, "Missing build generator for shell %q", shell)
   373  
   374  		multilineBuild, err := buildGenerator()
   375  		require.NoError(t, err)
   376  
   377  		build, cleanup := newBuild(t, multilineBuild, shell)
   378  		defer cleanup()
   379  
   380  		// The default build shouldn't have debug tracing enabled
   381  		out, err := runBuildReturningOutput(t, build)
   382  		assert.NoError(t, err)
   383  		assert.NotContains(t, out, "echo")
   384  		assert.Contains(t, out, "Hello World")
   385  		assert.Contains(t, out, "collapsed multi-line command")
   386  	})
   387  }
   388  
   389  func TestBuildWithGoodGitSSLCAInfo(t *testing.T) {
   390  	shellstest.OnEachShell(t, func(t *testing.T, shell string) {
   391  		if shell == "cmd" {
   392  			t.Skip("This test doesn't support Windows CMD (which is deprecated)")
   393  		}
   394  
   395  		successfulBuild, err := common.GetRemoteGitLabComTLSBuild()
   396  		require.NoError(t, err)
   397  
   398  		build, cleanup := newBuild(t, successfulBuild, shell)
   399  		defer cleanup()
   400  
   401  		build.Runner.URL = "https://gitlab.com"
   402  
   403  		out, err := runBuildReturningOutput(t, build)
   404  		assert.NoError(t, err)
   405  		assert.Contains(t, out, "Created fresh repository")
   406  		assert.Contains(t, out, "Updating/initializing submodules")
   407  	})
   408  }
   409  
   410  // TestBuildWithGitSSLAndStrategyFetch describes issue https://gitlab.com/gitlab-org/gitlab-runner/issues/2991
   411  func TestBuildWithGitSSLAndStrategyFetch(t *testing.T) {
   412  	shellstest.OnEachShell(t, func(t *testing.T, shell string) {
   413  		successfulBuild, err := common.GetRemoteGitLabComTLSBuild()
   414  		require.NoError(t, err)
   415  
   416  		build, cleanup := newBuild(t, successfulBuild, shell)
   417  		defer cleanup()
   418  
   419  		build.Runner.PreCloneScript = "echo pre-clone-script"
   420  		build.Variables = append(build.Variables, common.JobVariable{Key: "GIT_STRATEGY", Value: "fetch"})
   421  
   422  		out, err := runBuildReturningOutput(t, build)
   423  		assert.NoError(t, err)
   424  		assert.Contains(t, out, "Created fresh repository")
   425  		assert.Regexp(t, "Checking out [a-f0-9]+ as", out)
   426  
   427  		out, err = runBuildReturningOutput(t, build)
   428  		assert.NoError(t, err)
   429  		assert.Contains(t, out, "Fetching changes")
   430  		assert.Regexp(t, "Checking out [a-f0-9]+ as", out)
   431  		assert.Contains(t, out, "pre-clone-script")
   432  	})
   433  }
   434  
   435  func TestBuildChangesBranchesWhenFetchingRepo(t *testing.T) {
   436  	shellstest.OnEachShell(t, func(t *testing.T, shell string) {
   437  		successfulBuild, err := common.GetRemoteSuccessfulBuild()
   438  		require.NoError(t, err)
   439  
   440  		build, cleanup := newBuild(t, successfulBuild, shell)
   441  		defer cleanup()
   442  		build.Variables = append(build.Variables, common.JobVariable{Key: "GIT_STRATEGY", Value: "fetch"})
   443  
   444  		out, err := runBuildReturningOutput(t, build)
   445  		assert.NoError(t, err)
   446  		assert.Contains(t, out, "Created fresh repository")
   447  
   448  		// Another build using the same repo but different branch.
   449  		build.GitInfo = common.GetLFSGitInfo(build.GitInfo.RepoURL)
   450  		out, err = runBuildReturningOutput(t, build)
   451  		assert.NoError(t, err)
   452  		assert.Contains(t, out, "Checking out 2371dd05 as add-lfs-object...")
   453  	})
   454  }
   455  
   456  func TestBuildPowerShellCatchesExceptions(t *testing.T) {
   457  	helpers.SkipIntegrationTests(t, "powershell")
   458  
   459  	successfulBuild, err := common.GetRemoteSuccessfulBuild()
   460  	require.NoError(t, err)
   461  
   462  	build, cleanup := newBuild(t, successfulBuild, "powershell")
   463  	defer cleanup()
   464  	build.Variables = append(build.Variables, common.JobVariable{Key: "ErrorActionPreference", Value: "Stop"})
   465  	build.Variables = append(build.Variables, common.JobVariable{Key: "GIT_STRATEGY", Value: "fetch"})
   466  
   467  	out, err := runBuildReturningOutput(t, build)
   468  	assert.NoError(t, err)
   469  	assert.Contains(t, out, "Created fresh repository")
   470  
   471  	out, err = runBuildReturningOutput(t, build)
   472  	assert.NoError(t, err)
   473  	assert.NotContains(t, out, "Created fresh repository")
   474  	assert.Regexp(t, "Checking out [a-f0-9]+ as", out)
   475  
   476  	build.Variables = append(build.Variables, common.JobVariable{Key: "ErrorActionPreference", Value: "Continue"})
   477  	out, err = runBuildReturningOutput(t, build)
   478  	assert.NoError(t, err)
   479  	assert.NotContains(t, out, "Created fresh repository")
   480  	assert.Regexp(t, "Checking out [a-f0-9]+ as", out)
   481  
   482  	build.Variables = append(build.Variables, common.JobVariable{Key: "ErrorActionPreference", Value: "SilentlyContinue"})
   483  	out, err = runBuildReturningOutput(t, build)
   484  	assert.NoError(t, err)
   485  	assert.NotContains(t, out, "Created fresh repository")
   486  	assert.Regexp(t, "Checking out [a-f0-9]+ as", out)
   487  }
   488  
   489  func TestBuildOnCustomDirectory(t *testing.T) {
   490  	commands := map[string]string{
   491  		"bash":       "pwd",
   492  		"powershell": "pwd",
   493  	}
   494  
   495  	tests := map[string]bool{
   496  		"custom directory defined":     true,
   497  		"custom directory not defined": false,
   498  	}
   499  
   500  	shellstest.OnEachShell(t, func(t *testing.T, shell string) {
   501  		if shell == "cmd" {
   502  			t.Skip("This test doesn't support Windows CMD (which is deprecated)")
   503  		}
   504  
   505  		for testName, tt := range tests {
   506  			t.Run(testName, func(t *testing.T) {
   507  				cmd, ok := commands[shell]
   508  				require.Truef(t, ok, "Missing command for shell %q", shell)
   509  
   510  				dir := filepath.Join(os.TempDir(), "custom", "directory")
   511  				expectedDirectory := filepath.Join(dir, "0")
   512  
   513  				successfulBuild, err := common.GetSuccessfulBuild()
   514  				require.NoError(t, err)
   515  
   516  				successfulBuild.Steps[0].Script = common.StepScript{cmd}
   517  
   518  				build, cleanup := newBuild(t, successfulBuild, shell)
   519  				defer cleanup()
   520  
   521  				if tt {
   522  					build.Variables = append(build.Variables, common.JobVariable{
   523  						Key:    "IS_RUN_ON_CUSTOM_DIR",
   524  						Value:  dir,
   525  						Public: true,
   526  					})
   527  				}
   528  
   529  				out, err := runBuildReturningOutput(t, build)
   530  				assert.NoError(t, err)
   531  
   532  				if tt {
   533  					assert.Contains(t, out, expectedDirectory)
   534  				} else {
   535  					assert.NotContains(t, out, expectedDirectory)
   536  				}
   537  			})
   538  		}
   539  	})
   540  }