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  }