github.com/chenbh/concourse/v6@v6.4.2/worker/runtime/integration/integration_test.go (about) 1 package integration_test 2 3 import ( 4 "bytes" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "os/exec" 9 "path/filepath" 10 "syscall" 11 "time" 12 13 "code.cloudfoundry.org/garden" 14 "github.com/chenbh/concourse/v6/worker/runtime" 15 "github.com/chenbh/concourse/v6/worker/runtime/libcontainerd" 16 "github.com/containerd/containerd" 17 "github.com/stretchr/testify/require" 18 "github.com/stretchr/testify/suite" 19 ) 20 21 //Note: Some of these integration tests call on functionality that manipulates 22 //the iptable rule set. They lack isolation and, therefore, should never be run in parallel. 23 type IntegrationSuite struct { 24 suite.Suite 25 *require.Assertions 26 27 gardenBackend runtime.GardenBackend 28 client *libcontainerd.Client 29 containerdProcess *exec.Cmd 30 rootfs string 31 stderr bytes.Buffer 32 stdout bytes.Buffer 33 tmpDir string 34 } 35 36 func (s *IntegrationSuite) containerdSocket() string { 37 return filepath.Join(s.tmpDir, "containerd.sock") 38 } 39 40 func (s *IntegrationSuite) startContainerd() { 41 command := exec.Command("containerd", 42 "--address="+s.containerdSocket(), 43 "--root="+filepath.Join(s.tmpDir, "root"), 44 "--state="+filepath.Join(s.tmpDir, "state"), 45 ) 46 47 command.Stdout = &s.stdout 48 command.Stderr = &s.stderr 49 command.SysProcAttr = &syscall.SysProcAttr{ 50 Pdeathsig: syscall.SIGKILL, 51 } 52 53 err := command.Start() 54 s.NoError(err) 55 56 s.containerdProcess = command 57 } 58 59 func (s *IntegrationSuite) stopContainerd() { 60 s.NoError(s.containerdProcess.Process.Signal(syscall.SIGTERM)) 61 s.NoError(s.containerdProcess.Wait()) 62 } 63 64 func (s *IntegrationSuite) SetupSuite() { 65 var err error 66 s.tmpDir, err = ioutil.TempDir("", "containerd") 67 s.NoError(err) 68 69 s.startContainerd() 70 71 retries := 0 72 for retries < 100 { 73 c, err := containerd.New(s.containerdSocket(), containerd.WithTimeout(100*time.Millisecond)) 74 if err != nil { 75 retries++ 76 continue 77 } 78 79 c.Close() 80 return 81 } 82 83 s.stopContainerd() 84 s.NoError(os.RemoveAll(s.tmpDir)) 85 86 fmt.Println("STDOUT:", s.stdout.String()) 87 fmt.Println("STDERR:", s.stderr.String()) 88 s.Fail("timed out waiting for containerd to start") 89 } 90 91 func (s *IntegrationSuite) TearDownSuite() { 92 s.stopContainerd() 93 s.NoError(os.RemoveAll(s.tmpDir)) 94 } 95 96 func (s *IntegrationSuite) SetupTest() { 97 var ( 98 err error 99 namespace = "test" 100 requestTimeout = 3 * time.Second 101 ) 102 103 s.gardenBackend, err = runtime.NewGardenBackend( 104 libcontainerd.New( 105 s.containerdSocket(), 106 namespace, 107 requestTimeout, 108 ), 109 ) 110 s.NoError(err) 111 s.NoError(s.gardenBackend.Start()) 112 113 s.setupRootfs() 114 } 115 116 func (s *IntegrationSuite) setupRootfs() { 117 var err error 118 119 s.rootfs, err = ioutil.TempDir("", "containerd-integration") 120 s.NoError(err) 121 122 cmd := exec.Command("go", "build", 123 "-tags", "netgo", 124 "-o", filepath.Join(s.rootfs, "executable"), 125 "./sample/main.go", 126 ) 127 128 err = cmd.Run() 129 s.NoError(err) 130 131 return 132 } 133 134 func (s *IntegrationSuite) TearDownTest() { 135 s.gardenBackend.Stop() 136 os.RemoveAll(s.rootfs) 137 s.cleanupIptables() 138 } 139 140 func (s *IntegrationSuite) cleanupIptables() { 141 //Flush all rules 142 exec.Command("iptables", "-F").Run() 143 //Delete all user-defined chains 144 exec.Command("iptables", "-X").Run() 145 } 146 147 func (s *IntegrationSuite) TestPing() { 148 s.NoError(s.gardenBackend.Ping()) 149 } 150 151 // TestContainerCreateRunStopDestroy validates that we're able to go through the 152 // whole lifecycle of: 153 // 154 // 1. creating the container 155 // 2. running a process in it 156 // 3. stopping the process 157 // 4. deleting the container 158 // 159 func (s *IntegrationSuite) TestContainerCreateRunStopedDestroy() { 160 handle := uuid() 161 properties := garden.Properties{"test": uuid()} 162 163 _, err := s.gardenBackend.Create(garden.ContainerSpec{ 164 Handle: handle, 165 RootFSPath: "raw://" + s.rootfs, 166 Privileged: true, 167 Properties: properties, 168 }) 169 s.NoError(err) 170 171 containers, err := s.gardenBackend.Containers(properties) 172 s.NoError(err) 173 174 s.Len(containers, 1) 175 176 err = s.gardenBackend.Destroy(handle) 177 s.NoError(err) 178 179 containers, err = s.gardenBackend.Containers(properties) 180 s.NoError(err) 181 s.Len(containers, 0) 182 } 183 184 // TestContainerNetworkEgress aims at verifying that a process that we run in a 185 // container that we create through our gardenBackend is able to make requests to 186 // external services. 187 // 188 func (s *IntegrationSuite) TestContainerNetworkEgress() { 189 handle := uuid() 190 191 container, err := s.gardenBackend.Create(garden.ContainerSpec{ 192 Handle: handle, 193 RootFSPath: "raw://" + s.rootfs, 194 Privileged: true, 195 }) 196 s.NoError(err) 197 198 defer func() { 199 s.NoError(s.gardenBackend.Destroy(handle)) 200 }() 201 202 buf := new(buffer) 203 proc, err := container.Run( 204 garden.ProcessSpec{ 205 Path: "/executable", 206 Args: []string{ 207 "-http-get=http://example.com", 208 }, 209 }, 210 garden.ProcessIO{ 211 Stdout: buf, 212 Stderr: buf, 213 }, 214 ) 215 s.NoError(err) 216 217 exitCode, err := proc.Wait() 218 s.NoError(err) 219 220 s.Equal(exitCode, 0) 221 s.Equal("200 OK\n", buf.String()) 222 } 223 224 // TestContainerNetworkEgressWithRestrictedNetworks verifies that a process that we run in a 225 // container that we create through our gardenBackend is not able to reach an address that 226 // we have blocked access to. 227 // 228 func (s *IntegrationSuite) TestContainerNetworkEgressWithRestrictedNetworks() { 229 namespace := "test-restricted-networks" 230 requestTimeout := 3 * time.Second 231 232 network, err := runtime.NewCNINetwork( 233 runtime.WithRestrictedNetworks([]string{"1.1.1.1"}), 234 ) 235 236 s.NoError(err) 237 238 networkOpt := runtime.WithNetwork(network) 239 customBackend, err := runtime.NewGardenBackend( 240 libcontainerd.New( 241 s.containerdSocket(), 242 namespace, 243 requestTimeout, 244 ), 245 networkOpt, 246 ) 247 s.NoError(err) 248 249 s.NoError(customBackend.Start()) 250 251 handle := uuid() 252 253 container, err := customBackend.Create(garden.ContainerSpec{ 254 Handle: handle, 255 RootFSPath: "raw://" + s.rootfs, 256 Privileged: true, 257 }) 258 s.NoError(err) 259 260 defer func() { 261 s.NoError(customBackend.Destroy(handle)) 262 customBackend.Stop() 263 }() 264 265 buf := new(buffer) 266 proc, err := container.Run( 267 garden.ProcessSpec{ 268 Path: "/executable", 269 Args: []string{ 270 "-http-get=http://1.1.1.1", 271 }, 272 }, 273 garden.ProcessIO{ 274 Stdout: buf, 275 Stderr: buf, 276 }, 277 ) 278 s.NoError(err) 279 280 exitCode, err := proc.Wait() 281 s.NoError(err) 282 283 s.Equal(exitCode, 1, "Process in container should not be able to connect to restricted network") 284 s.Contains(buf.String(), "connect: connection refused") 285 } 286 287 // TestRunPrivileged tests whether we're able to run a process in a privileged 288 // container. 289 // 290 func (s *IntegrationSuite) TestRunPrivileged() { 291 s.runToCompletion(true) 292 } 293 294 // TestRunPrivileged tests whether we're able to run a process in an 295 // unprivileged container. 296 // 297 // Differently from the privileged counterpart, we first need to change the 298 // ownership of the rootfs so the uid 0 inside the container has the permissions 299 // to execute the executable in there. 300 // 301 func (s *IntegrationSuite) TestRunUnprivileged() { 302 maxUid, maxGid, err := runtime.NewUserNamespace().MaxValidIds() 303 s.NoError(err) 304 305 filepath.Walk(s.rootfs, func(path string, _ os.FileInfo, _ error) error { 306 return os.Lchown(path, int(maxUid), int(maxGid)) 307 }) 308 309 s.runToCompletion(false) 310 } 311 312 func (s *IntegrationSuite) runToCompletion(privileged bool) { 313 handle := uuid() 314 315 container, err := s.gardenBackend.Create(garden.ContainerSpec{ 316 Handle: handle, 317 RootFSPath: "raw://" + s.rootfs, 318 Privileged: privileged, 319 Env: []string{ 320 "FOO=bar", 321 }, 322 }) 323 s.NoError(err) 324 325 defer func() { 326 s.NoError(s.gardenBackend.Destroy(handle)) 327 }() 328 329 buf := new(buffer) 330 proc, err := container.Run( 331 garden.ProcessSpec{ 332 Path: "/executable", 333 Dir: "/somewhere", 334 }, 335 garden.ProcessIO{ 336 Stdout: buf, 337 Stderr: buf, 338 }, 339 ) 340 s.NoError(err) 341 342 exitCode, err := proc.Wait() 343 s.NoError(err) 344 345 s.Equal(exitCode, 0) 346 s.Equal("hello world\n", buf.String()) 347 348 } 349 350 // TestAttachToUnknownProc verifies that trying to attach to a process that does 351 // not exist lead to an error. 352 // 353 func (s *IntegrationSuite) TestAttachToUnknownProc() { 354 handle := uuid() 355 356 container, err := s.gardenBackend.Create(garden.ContainerSpec{ 357 Handle: handle, 358 RootFSPath: "raw://" + s.rootfs, 359 Privileged: true, 360 }) 361 s.NoError(err) 362 363 defer func() { 364 s.NoError(s.gardenBackend.Destroy(handle)) 365 }() 366 367 _, err = container.Attach("inexistent", garden.ProcessIO{ 368 Stdout: ioutil.Discard, 369 Stderr: ioutil.Discard, 370 }) 371 s.Error(err) 372 } 373 374 // TestAttach tries to validate that we're able to start a process in a 375 // container, get rid of the original client that originated the process, and 376 // then attach back to that process from a new client. 377 // 378 func (s *IntegrationSuite) TestAttach() { 379 handle := uuid() 380 381 container, err := s.gardenBackend.Create(garden.ContainerSpec{ 382 Handle: handle, 383 RootFSPath: "raw://" + s.rootfs, 384 Privileged: true, 385 }) 386 s.NoError(err) 387 388 lockedBuffer := new(buffer) 389 lockedBuffer.Lock() 390 391 originalProc, err := container.Run( 392 garden.ProcessSpec{ 393 Path: "/executable", 394 Args: []string{ 395 "-write-many-times=aa", 396 }, 397 }, 398 garden.ProcessIO{ 399 Stdout: lockedBuffer, 400 Stderr: lockedBuffer, 401 }, 402 ) 403 s.NoError(err) 404 405 id := originalProc.ID() 406 407 // kill the conn, and attach 408 409 s.gardenBackend.Stop() 410 s.NoError(s.gardenBackend.Start()) 411 412 container, err = s.gardenBackend.Lookup(handle) 413 s.NoError(err) 414 415 buf := new(buffer) 416 proc, err := container.Attach(id, garden.ProcessIO{ 417 Stdout: buf, 418 Stderr: buf, 419 }) 420 s.NoError(err) 421 422 exitCode, err := proc.Wait() 423 s.NoError(err) 424 425 s.Equal(exitCode, 0) 426 s.Contains(buf.String(), "aa\naa\naa\naa\naa\naa\n") 427 428 err = s.gardenBackend.Destroy(container.Handle()) 429 s.NoError(err) 430 } 431 432 // TestCustomDNS verfies that when a network is setup with custom NameServers 433 // those NameServers should appear in the container's etc/resolv.conf 434 // 435 func (s *IntegrationSuite) TestCustomDNS() { 436 namespace := "test-custom-dns" 437 requestTimeout := 3 * time.Second 438 439 network, err := runtime.NewCNINetwork( 440 runtime.WithNameServers([]string{ 441 "1.1.1.1", "1.2.3.4", 442 }), 443 ) 444 s.NoError(err) 445 446 networkOpt := runtime.WithNetwork(network) 447 customBackend, err := runtime.NewGardenBackend( 448 libcontainerd.New( 449 s.containerdSocket(), 450 namespace, 451 requestTimeout, 452 ), 453 networkOpt, 454 ) 455 s.NoError(err) 456 457 s.NoError(customBackend.Start()) 458 459 handle := uuid() 460 461 container, err := customBackend.Create(garden.ContainerSpec{ 462 Handle: handle, 463 RootFSPath: "raw://" + s.rootfs, 464 Privileged: true, 465 }) 466 s.NoError(err) 467 468 defer func() { 469 s.NoError(customBackend.Destroy(handle)) 470 customBackend.Stop() 471 }() 472 473 buf := new(buffer) 474 475 proc, err := container.Run( 476 garden.ProcessSpec{ 477 Path: "/executable", 478 Args: []string{ 479 "-cat", 480 "/etc/resolv.conf", 481 }, 482 }, 483 garden.ProcessIO{ 484 Stdout: buf, 485 Stderr: buf, 486 }, 487 ) 488 s.NoError(err) 489 490 exitCode, err := proc.Wait() 491 s.NoError(err) 492 493 s.Equal(exitCode, 0) 494 expectedDNSServer := "nameserver 1.1.1.1\nnameserver 1.2.3.4\n" 495 s.Equal(expectedDNSServer, buf.String()) 496 } 497 498 // TestUngracefulStop aims at validating that we're giving the process enough 499 // opportunity to finish, but that at the same time, we don't wait forever. 500 // 501 func (s *IntegrationSuite) TestUngracefulStop() { 502 var ungraceful = true 503 s.testStop(ungraceful) 504 } 505 506 // TestGracefulStop aims at validating that we're giving the process enough 507 // opportunity to finish, but that at the same time, we don't wait forever. 508 // 509 func (s *IntegrationSuite) TestGracefulStop() { 510 var ungraceful = false 511 s.testStop(ungraceful) 512 } 513 514 func (s *IntegrationSuite) testStop(kill bool) { 515 handle := uuid() 516 container, err := s.gardenBackend.Create(garden.ContainerSpec{ 517 Handle: handle, 518 RootFSPath: "raw://" + s.rootfs, 519 Privileged: true, 520 }) 521 s.NoError(err) 522 523 defer func() { 524 s.NoError(s.gardenBackend.Destroy(handle)) 525 }() 526 527 _, err = container.Run( 528 garden.ProcessSpec{ 529 Path: "/executable", 530 Args: []string{"-wait-for-signal=sighup"}, 531 }, 532 garden.ProcessIO{ 533 Stdout: os.Stdout, 534 Stderr: os.Stderr, 535 }, 536 ) 537 s.NoError(err) 538 s.NoError(container.Stop(kill)) 539 } 540 541 // TestMaxContainers aims at making sure that when the max container count is 542 // reached, any additional Create calls will fail 543 // 544 func (s *IntegrationSuite) TestMaxContainers() { 545 namespace := "test-max-containers" 546 requestTimeout := 1 * time.Second 547 548 limit := runtime.WithMaxContainers(1) 549 550 customBackend, err := runtime.NewGardenBackend( 551 libcontainerd.New( 552 s.containerdSocket(), 553 namespace, 554 requestTimeout, 555 ), 556 limit, 557 ) 558 s.NoError(err) 559 560 s.NoError(customBackend.Start()) 561 562 handle1 := uuid() 563 handle2 := uuid() 564 565 _, err = customBackend.Create(garden.ContainerSpec{ 566 Handle: handle1, 567 RootFSPath: "raw://" + s.rootfs, 568 Privileged: true, 569 }) 570 s.NoError(err) 571 572 defer func() { 573 s.NoError(customBackend.Destroy(handle1)) 574 customBackend.Stop() 575 }() 576 577 // not destroying handle2 as it is never successfully created 578 _, err = customBackend.Create(garden.ContainerSpec{ 579 Handle: handle2, 580 RootFSPath: "raw://" + s.rootfs, 581 Privileged: true, 582 }) 583 s.Error(err) 584 s.Contains(err.Error(), "max containers reached") 585 }