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 }