github.com/openshift/source-to-image@v1.4.1-0.20240516041539-bf52fc02204e/test/integration/docker/integration_test.go (about)

     1  //go:build integration
     2  // +build integration
     3  
     4  package docker
     5  
     6  import (
     7  	"encoding/json"
     8  	"errors"
     9  	"flag"
    10  	"fmt"
    11  	"io/ioutil"
    12  	"net"
    13  	"net/http"
    14  	"net/http/httptest"
    15  	"os"
    16  	"path/filepath"
    17  	"runtime"
    18  	"testing"
    19  	"time"
    20  
    21  	"k8s.io/klog/v2"
    22  
    23  	dockertypes "github.com/docker/docker/api/types"
    24  	dockercontainer "github.com/docker/docker/api/types/container"
    25  	dockerapi "github.com/docker/docker/client"
    26  	"golang.org/x/net/context"
    27  
    28  	"github.com/openshift/source-to-image/pkg/api"
    29  	"github.com/openshift/source-to-image/pkg/build"
    30  	"github.com/openshift/source-to-image/pkg/build/strategies"
    31  	"github.com/openshift/source-to-image/pkg/docker"
    32  	dockerpkg "github.com/openshift/source-to-image/pkg/docker"
    33  	"github.com/openshift/source-to-image/pkg/scm/git"
    34  	"github.com/openshift/source-to-image/pkg/tar"
    35  	"github.com/openshift/source-to-image/pkg/util"
    36  	"github.com/openshift/source-to-image/pkg/util/fs"
    37  )
    38  
    39  const (
    40  	DefaultDockerSocket = "unix:///var/run/docker.sock"
    41  	TestSource          = "https://github.com/openshift/ruby-hello-world"
    42  
    43  	FakeBuilderImage                = "sti_test/sti-fake"
    44  	FakeUserImage                   = "sti_test/sti-fake-user"
    45  	FakeImageScripts                = "sti_test/sti-fake-scripts"
    46  	FakeImageScriptsNoSaveArtifacts = "sti_test/sti-fake-scripts-no-save-artifacts"
    47  	FakeImageNoTar                  = "sti_test/sti-fake-no-tar"
    48  	FakeImageOnBuild                = "sti_test/sti-fake-onbuild"
    49  	FakeNumericUserImage            = "sti_test/sti-fake-numericuser"
    50  	FakeImageOnBuildRootUser        = "sti_test/sti-fake-onbuild-rootuser"
    51  	FakeImageOnBuildNumericUser     = "sti_test/sti-fake-onbuild-numericuser"
    52  	FakeImageAssembleRoot           = "sti_test/sti-fake-assemble-root"
    53  	FakeImageAssembleUser           = "sti_test/sti-fake-assemble-user"
    54  
    55  	TagCleanBuild                              = "test/sti-fake-app"
    56  	TagCleanBuildUser                          = "test/sti-fake-app-user"
    57  	TagIncrementalBuild                        = "test/sti-incremental-app"
    58  	TagIncrementalBuildUser                    = "test/sti-incremental-app-user"
    59  	TagCleanBuildScripts                       = "test/sti-fake-app-scripts"
    60  	TagIncrementalBuildScripts                 = "test/sti-incremental-app-scripts"
    61  	TagIncrementalBuildScriptsNoSaveArtifacts  = "test/sti-incremental-app-scripts-no-save-artifacts"
    62  	TagCleanLayeredBuildNoTar                  = "test/sti-fake-no-tar"
    63  	TagCleanBuildOnBuild                       = "test/sti-fake-app-onbuild"
    64  	TagIncrementalBuildOnBuild                 = "test/sti-incremental-app-onbuild"
    65  	TagCleanBuildOnBuildNoName                 = "test/sti-fake-app-onbuild-noname"
    66  	TagCleanBuildNoName                        = "test/sti-fake-app-noname"
    67  	TagCleanLayeredBuildNoTarNoName            = "test/sti-fake-no-tar-noname"
    68  	TagCleanBuildAllowedUIDsNamedUser          = "test/sti-fake-alloweduids-nameduser"
    69  	TagCleanBuildAllowedUIDsNumericUser        = "test/sti-fake-alloweduids-numericuser"
    70  	TagCleanBuildAllowedUIDsOnBuildRoot        = "test/sti-fake-alloweduids-onbuildroot"
    71  	TagCleanBuildAllowedUIDsOnBuildNumericUser = "test/sti-fake-alloweduids-onbuildnumeric"
    72  	TagCleanBuildAllowedUIDsAssembleRoot       = "test/sti-fake-alloweduids-assembleroot"
    73  	TagCleanBuildAllowedUIDsAssembleUser       = "test/sti-fake-alloweduids-assembleuser"
    74  
    75  	// Need to serve the scripts from local host so any potential changes to the
    76  	// scripts are made available for integration testing.
    77  	//
    78  	// Port 23456 must match the port used in the fake image Dockerfiles
    79  	FakeScriptsHTTPURL = "http://127.0.0.1:23456/.s2i/bin"
    80  )
    81  
    82  var engineClient docker.Client
    83  
    84  func init() {
    85  	klog.InitFlags(nil)
    86  
    87  	var err error
    88  	engineClient, err = docker.NewEngineAPIClient(docker.GetDefaultDockerConfig())
    89  	if err != nil {
    90  		panic(err)
    91  	}
    92  
    93  	// get the full path to this .go file so we can construct the file url
    94  	// using this file's dirname
    95  	_, filename, _, _ := runtime.Caller(0)
    96  	testImagesDir := filepath.Join(filepath.Dir(filepath.Dir(filename)), "scripts")
    97  
    98  	l, err := net.Listen("tcp", ":23456")
    99  	if err != nil {
   100  		panic(err)
   101  	}
   102  
   103  	hs := http.Server{Handler: http.FileServer(http.Dir(testImagesDir))}
   104  	hs.SetKeepAlivesEnabled(false)
   105  	go hs.Serve(l)
   106  }
   107  
   108  func getDefaultContext() (context.Context, context.CancelFunc) {
   109  	return context.WithTimeout(context.Background(), 20*time.Second)
   110  }
   111  
   112  // TestInjectionBuild tests the build where we inject files to assemble script.
   113  func TestInjectionBuild(t *testing.T) {
   114  	tempdir, err := ioutil.TempDir("", "s2i-test-dir")
   115  	if err != nil {
   116  		t.Errorf("Unable to create temporary directory: %v", err)
   117  	}
   118  	defer os.RemoveAll(tempdir)
   119  
   120  	err = ioutil.WriteFile(filepath.Join(tempdir, "secret"), []byte("secret"), 0666)
   121  	if err != nil {
   122  		t.Errorf("Unable to write content to temporary injection file: %v", err)
   123  	}
   124  
   125  	integration(t).exerciseInjectionBuild(TagCleanBuild, FakeBuilderImage, []string{
   126  		tempdir + ":/tmp",
   127  		tempdir + ":",
   128  		tempdir + ":test;" + tempdir + ":test2",
   129  	}, true)
   130  }
   131  
   132  func TestInjectionBuildBadDestination(t *testing.T) {
   133  	tempdir, err := ioutil.TempDir("", "s2i-test-dir")
   134  	if err != nil {
   135  		t.Errorf("Unable to create temporary directory: %v", err)
   136  	}
   137  	defer os.RemoveAll(tempdir)
   138  
   139  	err = ioutil.WriteFile(filepath.Join(tempdir, "secret"), []byte("secret"), 0666)
   140  	if err != nil {
   141  		t.Errorf("Unable to write content to temporary injection file: %v", err)
   142  	}
   143  
   144  	integration(t).exerciseInjectionBuild(TagCleanBuild, FakeBuilderImage, []string{tempdir + ":/bad/dir"}, false)
   145  }
   146  
   147  type integrationTest struct {
   148  	t             *testing.T
   149  	setupComplete bool
   150  }
   151  
   152  func (i integrationTest) InspectImage(name string) (*dockertypes.ImageInspect, error) {
   153  	ctx, cancel := getDefaultContext()
   154  	defer cancel()
   155  	resp, _, err := engineClient.ImageInspectWithRaw(ctx, name)
   156  	if err != nil {
   157  		if dockerapi.IsErrNotFound(err) {
   158  			return nil, fmt.Errorf("no such image :%q", name)
   159  		}
   160  		return nil, err
   161  	}
   162  	return &resp, nil
   163  }
   164  
   165  var (
   166  	FakeScriptsFileURL string
   167  )
   168  
   169  func getLogLevel() (level int) {
   170  	for level = 0; level <= 5; level++ {
   171  		if klog.V(klog.Level(level)).Enabled() {
   172  			break
   173  		}
   174  	}
   175  	return
   176  }
   177  
   178  // setup sets up integration tests
   179  func (i *integrationTest) setup() {
   180  	if !i.setupComplete {
   181  		// get the full path to this .go file so we can construct the file url
   182  		// using this file's dirname
   183  		_, filename, _, _ := runtime.Caller(0)
   184  
   185  		testImagesDir := filepath.Join(filepath.Dir(filepath.Dir(filename)), "scripts")
   186  		FakeScriptsFileURL = "file://" + filepath.ToSlash(filepath.Join(testImagesDir, ".s2i", "bin"))
   187  
   188  		for _, image := range []string{TagCleanBuild, TagCleanBuildUser, TagIncrementalBuild, TagIncrementalBuildUser} {
   189  			ctx, cancel := getDefaultContext()
   190  			engineClient.ImageRemove(ctx, image, dockertypes.ImageRemoveOptions{})
   191  			cancel()
   192  		}
   193  
   194  		i.setupComplete = true
   195  	}
   196  
   197  	from := flag.CommandLine
   198  	if vflag := from.Lookup("v"); vflag != nil {
   199  		// the thing here is that we are looking for the bash -v passed into test-integration.sh (with no value),
   200  		// but for klog (https://k8s.io/klog/v2/blob/master/klog.go), one specifies
   201  		// the logging level with -v=# (i.e. -v=0 or -v=3 or -v=5).
   202  		// so, for the changes stemming from issue 133, we 'reuse' the bash -v, and set the highest klog level.
   203  		// (if you look at STI's main.go, and setupGlog, it essentially maps klog's -v to --loglevel for use by the sti command)
   204  		//NOTE - passing --loglevel or -v=5 into test-integration.sh does not work
   205  		if getLogLevel() != 5 {
   206  			vflag.Value.Set("5")
   207  			// FIXME currently klog has only option to redirect output to stderr
   208  			// the preferred for STI would be to redirect to stdout
   209  			flag.CommandLine.Set("logtostderr", "true")
   210  		}
   211  	}
   212  }
   213  
   214  func integration(t *testing.T) *integrationTest {
   215  	i := &integrationTest{t: t}
   216  	i.setup()
   217  	return i
   218  }
   219  
   220  // Test a clean build.  The simplest case.
   221  func TestCleanBuild(t *testing.T) {
   222  	integration(t).exerciseCleanBuild(TagCleanBuild, false, FakeBuilderImage, "", true, true, false)
   223  }
   224  
   225  // Test Labels
   226  func TestCleanBuildLabel(t *testing.T) {
   227  	integration(t).exerciseCleanBuild(TagCleanBuild, false, FakeBuilderImage, "", true, true, true)
   228  }
   229  
   230  func TestCleanBuildUser(t *testing.T) {
   231  	integration(t).exerciseCleanBuild(TagCleanBuildUser, false, FakeUserImage, "", true, true, false)
   232  }
   233  
   234  func TestCleanBuildFileScriptsURL(t *testing.T) {
   235  	integration(t).exerciseCleanBuild(TagCleanBuild, false, FakeBuilderImage, FakeScriptsFileURL, true, true, false)
   236  }
   237  
   238  func TestCleanBuildHttpScriptsURL(t *testing.T) {
   239  	integration(t).exerciseCleanBuild(TagCleanBuild, false, FakeBuilderImage, FakeScriptsHTTPURL, true, true, false)
   240  }
   241  
   242  func TestCleanBuildScripts(t *testing.T) {
   243  	integration(t).exerciseCleanBuild(TagCleanBuildScripts, false, FakeImageScripts, "", true, true, false)
   244  }
   245  
   246  func TestLayeredBuildNoTar(t *testing.T) {
   247  	integration(t).exerciseCleanBuild(TagCleanLayeredBuildNoTar, false, FakeImageNoTar, FakeScriptsFileURL, false, true, false)
   248  }
   249  
   250  // Test that a build config with a callbackURL will invoke HTTP endpoint
   251  func TestCleanBuildCallbackInvoked(t *testing.T) {
   252  	integration(t).exerciseCleanBuild(TagCleanBuild, true, FakeBuilderImage, "", true, true, false)
   253  }
   254  
   255  func TestCleanBuildOnBuild(t *testing.T) {
   256  	integration(t).exerciseCleanBuild(TagCleanBuildOnBuild, false, FakeImageOnBuild, "", true, true, false)
   257  }
   258  
   259  func TestCleanBuildOnBuildNoName(t *testing.T) {
   260  	integration(t).exerciseCleanBuild(TagCleanBuildOnBuildNoName, false, FakeImageOnBuild, "", false, false, false)
   261  }
   262  
   263  func TestCleanBuildNoName(t *testing.T) {
   264  	integration(t).exerciseCleanBuild(TagCleanBuildNoName, false, FakeBuilderImage, "", true, false, false)
   265  }
   266  
   267  func TestLayeredBuildNoTarNoName(t *testing.T) {
   268  	integration(t).exerciseCleanBuild(TagCleanLayeredBuildNoTarNoName, false, FakeImageNoTar, FakeScriptsFileURL, false, false, false)
   269  }
   270  
   271  func TestAllowedUIDsNamedUser(t *testing.T) {
   272  	integration(t).exerciseCleanAllowedUIDsBuild(TagCleanBuildAllowedUIDsNamedUser, FakeUserImage, true)
   273  }
   274  
   275  func TestAllowedUIDsNumericUser(t *testing.T) {
   276  	integration(t).exerciseCleanAllowedUIDsBuild(TagCleanBuildAllowedUIDsNumericUser, FakeNumericUserImage, false)
   277  }
   278  
   279  func TestAllowedUIDsOnBuildRootUser(t *testing.T) {
   280  	integration(t).exerciseCleanAllowedUIDsBuild(TagCleanBuildAllowedUIDsNamedUser, FakeImageOnBuildRootUser, true)
   281  }
   282  
   283  func TestAllowedUIDsOnBuildNumericUser(t *testing.T) {
   284  	integration(t).exerciseCleanAllowedUIDsBuild(TagCleanBuildAllowedUIDsNumericUser, FakeImageOnBuildNumericUser, false)
   285  }
   286  
   287  func TestAllowedUIDsAssembleRoot(t *testing.T) {
   288  	integration(t).exerciseCleanAllowedUIDsBuild(TagCleanBuildAllowedUIDsAssembleRoot, FakeImageAssembleRoot, true)
   289  }
   290  
   291  func TestAllowedUIDsAssembleUser(t *testing.T) {
   292  	integration(t).exerciseCleanAllowedUIDsBuild(TagCleanBuildAllowedUIDsAssembleUser, FakeImageAssembleUser, false)
   293  }
   294  
   295  func (i *integrationTest) exerciseCleanAllowedUIDsBuild(tag, imageName string, expectError bool) {
   296  	t := i.t
   297  	config := &api.Config{
   298  		DockerConfig:      docker.GetDefaultDockerConfig(),
   299  		BuilderImage:      imageName,
   300  		BuilderPullPolicy: api.DefaultBuilderPullPolicy,
   301  		Source:            git.MustParse(TestSource),
   302  		Tag:               tag,
   303  		Incremental:       false,
   304  		ScriptsURL:        "",
   305  		ExcludeRegExp:     tar.DefaultExclusionPattern.String(),
   306  	}
   307  	config.AllowedUIDs.Set("1-")
   308  	_, _, err := strategies.Strategy(engineClient, config, build.Overrides{})
   309  	if err != nil && !expectError {
   310  		t.Fatalf("Cannot create a new builder: %v", err)
   311  	}
   312  	if err == nil && expectError {
   313  		t.Fatalf("Did not get an error and was expecting one.")
   314  	}
   315  }
   316  
   317  func (i *integrationTest) exerciseCleanBuild(tag string, verifyCallback bool, imageName string, scriptsURL string, expectImageName bool, setTag bool, checkLabel bool) {
   318  	t := i.t
   319  	callbackURL := ""
   320  	callbackInvoked := false
   321  	callbackHasValidJSON := false
   322  	if verifyCallback {
   323  		handler := func(w http.ResponseWriter, r *http.Request) {
   324  			// we got called
   325  			callbackInvoked = true
   326  			// the header is as expected
   327  			contentType := r.Header["Content-Type"][0]
   328  			callbackHasValidJSON = contentType == "application/json"
   329  			// the request body is as expected
   330  			if callbackHasValidJSON {
   331  				defer r.Body.Close()
   332  				body, _ := ioutil.ReadAll(r.Body)
   333  				type CallbackMessage struct {
   334  					Success bool
   335  					Labels  map[string]string
   336  				}
   337  				var callbackMessage CallbackMessage
   338  				err := json.Unmarshal(body, &callbackMessage)
   339  				callbackHasValidJSON = (err == nil) && callbackMessage.Success && len(callbackMessage.Labels) > 0
   340  			}
   341  		}
   342  		ts := httptest.NewServer(http.HandlerFunc(handler))
   343  		defer ts.Close()
   344  		callbackURL = ts.URL
   345  	}
   346  
   347  	var buildTag string
   348  	if setTag {
   349  		buildTag = tag
   350  	} else {
   351  		buildTag = ""
   352  	}
   353  
   354  	config := &api.Config{
   355  		DockerConfig:      docker.GetDefaultDockerConfig(),
   356  		BuilderImage:      imageName,
   357  		BuilderPullPolicy: api.DefaultBuilderPullPolicy,
   358  		Source:            git.MustParse(TestSource),
   359  		Tag:               buildTag,
   360  		Incremental:       false,
   361  		CallbackURL:       callbackURL,
   362  		ScriptsURL:        scriptsURL,
   363  		ExcludeRegExp:     tar.DefaultExclusionPattern.String(),
   364  	}
   365  
   366  	b, _, err := strategies.Strategy(engineClient, config, build.Overrides{})
   367  	if err != nil {
   368  		t.Fatalf("Cannot create a new builder.")
   369  	}
   370  	resp, err := b.Build(config)
   371  	if err != nil {
   372  		t.Fatalf("An error occurred during the build: %v", err)
   373  	} else if !resp.Success {
   374  		t.Fatalf("The build failed.")
   375  	}
   376  	if callbackInvoked != verifyCallback {
   377  		t.Fatalf("S2I build did not invoke callback")
   378  	}
   379  	if callbackHasValidJSON != verifyCallback {
   380  		t.Fatalf("S2I build did not invoke callback with valid json message")
   381  	}
   382  
   383  	// We restrict this check to only when we are passing tag through the build config
   384  	// since we will not end up with an available tag by that name from build
   385  	if setTag {
   386  		i.checkForImage(tag)
   387  		containerID := i.createContainer(tag)
   388  		i.checkBasicBuildState(containerID, resp.WorkingDir)
   389  
   390  		if checkLabel {
   391  			i.checkForLabel(tag)
   392  		}
   393  
   394  		i.removeContainer(containerID)
   395  	}
   396  
   397  	// Check if we receive back an ImageID when we are expecting to
   398  	if expectImageName && len(resp.ImageID) == 0 {
   399  		t.Fatalf("S2I build did not receive an ImageID in response")
   400  	}
   401  	if !expectImageName && len(resp.ImageID) > 0 {
   402  		t.Fatalf("S2I build received an ImageID in response")
   403  	}
   404  }
   405  
   406  // Test an incremental build.
   407  func TestIncrementalBuildAndRemovePreviousImage(t *testing.T) {
   408  	integration(t).exerciseIncrementalBuild(TagIncrementalBuild, FakeBuilderImage, true, false, false)
   409  }
   410  
   411  func TestIncrementalBuildAndKeepPreviousImage(t *testing.T) {
   412  	integration(t).exerciseIncrementalBuild(TagIncrementalBuild, FakeBuilderImage, false, false, false)
   413  }
   414  
   415  func TestIncrementalBuildUser(t *testing.T) {
   416  	integration(t).exerciseIncrementalBuild(TagIncrementalBuildUser, FakeBuilderImage, true, false, false)
   417  }
   418  
   419  func TestIncrementalBuildScripts(t *testing.T) {
   420  	integration(t).exerciseIncrementalBuild(TagIncrementalBuildScripts, FakeImageScripts, true, false, false)
   421  }
   422  
   423  func TestIncrementalBuildScriptsNoSaveArtifacts(t *testing.T) {
   424  	integration(t).exerciseIncrementalBuild(TagIncrementalBuildScriptsNoSaveArtifacts, FakeImageScriptsNoSaveArtifacts, true, true, false)
   425  }
   426  
   427  func TestIncrementalBuildOnBuild(t *testing.T) {
   428  	integration(t).exerciseIncrementalBuild(TagIncrementalBuildOnBuild, FakeImageOnBuild, false, true, true)
   429  }
   430  
   431  func (i *integrationTest) exerciseInjectionBuild(tag, imageName string, injections []string, expectSuccess bool) {
   432  	t := i.t
   433  
   434  	injectionList := api.VolumeList{}
   435  	for _, i := range injections {
   436  		err := injectionList.Set(i)
   437  		if err != nil {
   438  			t.Errorf("injectionList.Set() failed with error %s\n", err)
   439  		}
   440  	}
   441  	// For test purposes, keep at least one injected source
   442  	var keptVolume *api.VolumeSpec
   443  	if len(injectionList) > 0 {
   444  		injectionList[0].Keep = true
   445  		keptVolume = &injectionList[0]
   446  	}
   447  	config := &api.Config{
   448  		DockerConfig:      docker.GetDefaultDockerConfig(),
   449  		BuilderImage:      imageName,
   450  		BuilderPullPolicy: api.DefaultBuilderPullPolicy,
   451  		Source:            git.MustParse(TestSource),
   452  		Tag:               tag,
   453  		Injections:        injectionList,
   454  		ExcludeRegExp:     tar.DefaultExclusionPattern.String(),
   455  	}
   456  	builder, _, err := strategies.Strategy(engineClient, config, build.Overrides{})
   457  	if err != nil {
   458  		t.Fatalf("Unable to create builder: %v", err)
   459  	}
   460  	resp, err := builder.Build(config)
   461  	if !expectSuccess {
   462  		if resp.Success {
   463  			t.Fatal("Success was returned, but should have failed")
   464  		}
   465  		return
   466  	}
   467  	if err != nil {
   468  		t.Fatalf("Unexpected error occurred during build: %v", err)
   469  	}
   470  	if !resp.Success {
   471  		t.Fatalf("S2I build failed.")
   472  	}
   473  	i.checkForImage(tag)
   474  	containerID := i.createContainer(tag)
   475  	defer i.removeContainer(containerID)
   476  
   477  	// Check that the injected file is delivered to assemble script
   478  	i.fileExists(containerID, "/sti-fake/secret-delivered")
   479  	i.fileExists(containerID, "/sti-fake/relative-secret-delivered")
   480  
   481  	// Make sure the injected file does not exists in resulting image
   482  	testFs := fs.NewFileSystem()
   483  	files, err := util.ListFilesToTruncate(testFs, injectionList)
   484  	if err != nil {
   485  		t.Errorf("Unexpected error: %v", err)
   486  	}
   487  	for _, f := range files {
   488  		if err = i.testFile(tag, f); err == nil {
   489  			t.Errorf("The file %q must be empty or not exist", f)
   490  		}
   491  	}
   492  	if keptVolume != nil {
   493  		keptFiles, err := util.ListFiles(testFs, *keptVolume)
   494  		if err != nil {
   495  			t.Errorf("Unexpected error: %v", err)
   496  		}
   497  		for _, f := range keptFiles {
   498  			if err = i.testFile(tag, f); err != nil {
   499  				t.Errorf("The file %q must exist and not be empty", f)
   500  			}
   501  		}
   502  	}
   503  }
   504  
   505  func (i *integrationTest) testFile(tag, path string) error {
   506  	exitCode := i.runInImage(tag, "test -s "+path)
   507  	if exitCode != 0 {
   508  		return fmt.Errorf("file %s does not exist or is empty in the container %s", path, tag)
   509  	}
   510  	return nil
   511  }
   512  
   513  func (i *integrationTest) exerciseIncrementalBuild(tag, imageName string, removePreviousImage bool, expectClean bool, checkOnBuild bool) {
   514  	t := i.t
   515  	start := time.Now()
   516  	config := &api.Config{
   517  		DockerConfig:        docker.GetDefaultDockerConfig(),
   518  		BuilderImage:        imageName,
   519  		BuilderPullPolicy:   api.DefaultBuilderPullPolicy,
   520  		Source:              git.MustParse(TestSource),
   521  		Tag:                 tag,
   522  		Incremental:         false,
   523  		RemovePreviousImage: removePreviousImage,
   524  		ExcludeRegExp:       tar.DefaultExclusionPattern.String(),
   525  	}
   526  
   527  	builder, _, err := strategies.Strategy(engineClient, config, build.Overrides{})
   528  	if err != nil {
   529  		t.Fatalf("Unable to create builder: %v", err)
   530  	}
   531  	resp, err := builder.Build(config)
   532  	if err != nil {
   533  		t.Fatalf("Unexpected error occurred during build: %v", err)
   534  	}
   535  	if !resp.Success {
   536  		t.Fatalf("S2I build failed.")
   537  	}
   538  
   539  	previousImageID := resp.ImageID
   540  	config = &api.Config{
   541  		DockerConfig:            docker.GetDefaultDockerConfig(),
   542  		BuilderImage:            imageName,
   543  		BuilderPullPolicy:       api.DefaultBuilderPullPolicy,
   544  		Source:                  git.MustParse(TestSource),
   545  		Tag:                     tag,
   546  		Incremental:             true,
   547  		RemovePreviousImage:     removePreviousImage,
   548  		PreviousImagePullPolicy: api.PullIfNotPresent,
   549  		ExcludeRegExp:           tar.DefaultExclusionPattern.String(),
   550  	}
   551  
   552  	builder, _, err = strategies.Strategy(engineClient, config, build.Overrides{})
   553  	if err != nil {
   554  		t.Fatalf("Unable to create incremental builder: %v", err)
   555  	}
   556  	resp, err = builder.Build(config)
   557  	if err != nil {
   558  		t.Fatalf("Unexpected error occurred during incremental build: %v", err)
   559  	}
   560  	if !resp.Success {
   561  		t.Fatalf("S2I incremental build failed.")
   562  	}
   563  
   564  	i.checkForImage(tag)
   565  	containerID := i.createContainer(tag)
   566  	defer i.removeContainer(containerID)
   567  	i.checkIncrementalBuildState(containerID, resp.WorkingDir, expectClean)
   568  
   569  	_, err = i.InspectImage(previousImageID)
   570  	if removePreviousImage {
   571  		if err == nil {
   572  			t.Errorf("Previous image %s not deleted", previousImageID)
   573  		}
   574  	} else {
   575  		if err != nil {
   576  			t.Errorf("Couldn't find previous image %s", previousImageID)
   577  		}
   578  	}
   579  
   580  	if checkOnBuild {
   581  		i.fileExists(containerID, "/sti-fake/src/onbuild")
   582  	}
   583  
   584  	if took := time.Since(start); took > docker.DefaultDockerTimeout {
   585  		// https://github.com/openshift/source-to-image/issues/301 is a
   586  		// case where incremental builds would get stuck until the
   587  		// timeout.
   588  		t.Errorf("Test took too long (%v), some operation may have gotten stuck waiting for the DefaultDockerTimeout (%v). Inspect the logs to find operations that took long.", took, docker.DefaultDockerTimeout)
   589  	}
   590  }
   591  
   592  // Support methods
   593  func (i *integrationTest) checkForImage(tag string) {
   594  	_, err := i.InspectImage(tag)
   595  	if err != nil {
   596  		i.t.Errorf("Couldn't find image with tag: %s", tag)
   597  	}
   598  }
   599  
   600  func (i *integrationTest) createContainer(image string) string {
   601  	ctx, cancel := getDefaultContext()
   602  	defer cancel()
   603  	opts := dockertypes.ContainerCreateConfig{Name: "", Config: &dockercontainer.Config{Image: image}}
   604  	container, err := engineClient.ContainerCreate(ctx, opts.Config, opts.HostConfig, opts.NetworkingConfig, nil, opts.Name)
   605  	if err != nil {
   606  		i.t.Errorf("Couldn't create container from image %s with error %+v", image, err)
   607  		return ""
   608  	}
   609  
   610  	ctx, cancel = getDefaultContext()
   611  	defer cancel()
   612  	err = engineClient.ContainerStart(ctx, container.ID, dockertypes.ContainerStartOptions{})
   613  	if err != nil {
   614  		i.t.Errorf("Couldn't start container: %s with error %+v", container.ID, err)
   615  		return ""
   616  	}
   617  
   618  	ctx, cancel = getDefaultContext()
   619  	defer cancel()
   620  	waitC, errC := engineClient.ContainerWait(ctx, container.ID, dockercontainer.WaitConditionNextExit)
   621  	select {
   622  	case result := <-waitC:
   623  		if result.StatusCode != 0 {
   624  			i.t.Errorf("Bad exit code from container: %d", result.StatusCode)
   625  			return ""
   626  		}
   627  	case err := <-errC:
   628  		if errors.Is(err, context.DeadlineExceeded) {
   629  			ctx, cancel := getDefaultContext()
   630  			defer cancel()
   631  			inspectInfo, err2 := engineClient.ContainerInspect(ctx, container.ID)
   632  			if err2 == nil && inspectInfo.State != nil && inspectInfo.State.Status == "exited" {
   633  				// it must have finished before we tried to wait for a not-exited->exited state change
   634  				i.t.Logf("timed out waiting for container %q: it had already exited; continuing", container.ID)
   635  				err = nil
   636  			}
   637  		}
   638  		if err != nil {
   639  			i.t.Errorf("Error waiting for container: %v", err)
   640  			return ""
   641  		}
   642  	}
   643  	return container.ID
   644  }
   645  
   646  func (i *integrationTest) runInContainer(image string, command []string) int {
   647  	ctx, cancel := getDefaultContext()
   648  	defer cancel()
   649  	opts := dockertypes.ContainerCreateConfig{Name: "", Config: &dockercontainer.Config{Image: image, AttachStdout: false, AttachStdin: false, Cmd: command}}
   650  	container, err := engineClient.ContainerCreate(ctx, opts.Config, opts.HostConfig, opts.NetworkingConfig, nil, opts.Name)
   651  	if err != nil {
   652  		i.t.Errorf("Couldn't create container from image %s err %+v", image, err)
   653  		return -1
   654  	}
   655  
   656  	ctx, cancel = getDefaultContext()
   657  	defer cancel()
   658  	err = engineClient.ContainerStart(ctx, container.ID, dockertypes.ContainerStartOptions{})
   659  	if err != nil {
   660  		i.t.Errorf("Couldn't start container: %s", container.ID)
   661  	}
   662  	ctx, cancel = getDefaultContext()
   663  	defer cancel()
   664  	waitC, errC := engineClient.ContainerWait(ctx, container.ID, dockercontainer.WaitConditionNextExit)
   665  	exitCode := -1
   666  	select {
   667  	case result := <-waitC:
   668  		exitCode = int(result.StatusCode)
   669  	case err := <-errC:
   670  		if errors.Is(err, context.DeadlineExceeded) {
   671  			ctx, cancel := getDefaultContext()
   672  			defer cancel()
   673  			inspectInfo, err2 := engineClient.ContainerInspect(ctx, container.ID)
   674  			if err2 == nil && inspectInfo.State != nil && inspectInfo.State.Status == "exited" {
   675  				// it must have finished before we tried to wait for a not-exited->exited state change
   676  				i.t.Logf("timed out waiting for container %q: it had already exited; continuing", container.ID)
   677  				exitCode = inspectInfo.State.ExitCode
   678  				err = nil
   679  			}
   680  		}
   681  		if err != nil {
   682  			i.t.Errorf("Couldn't wait for container: %s: %v", container.ID, err)
   683  		}
   684  	}
   685  	ctx, cancel = getDefaultContext()
   686  	defer cancel()
   687  	err = engineClient.ContainerRemove(ctx, container.ID, dockertypes.ContainerRemoveOptions{})
   688  	if err != nil {
   689  		i.t.Errorf("Couldn't remove container: %s", container.ID)
   690  	}
   691  	return exitCode
   692  }
   693  
   694  func (i *integrationTest) removeContainer(cID string) {
   695  	ctx, cancel := getDefaultContext()
   696  	defer cancel()
   697  	engineClient.ContainerKill(ctx, cID, "SIGKILL")
   698  	removeOpts := dockertypes.ContainerRemoveOptions{
   699  		RemoveVolumes: true,
   700  	}
   701  	err := engineClient.ContainerRemove(ctx, cID, removeOpts)
   702  	if err != nil {
   703  		i.t.Errorf("Couldn't remove container %s: %s", cID, err)
   704  	}
   705  }
   706  
   707  func (i *integrationTest) fileExists(cID string, filePath string) {
   708  	res := i.fileExistsInContainer(cID, filePath)
   709  
   710  	if !res {
   711  		i.t.Errorf("Couldn't find file %s in container %s", filePath, cID)
   712  	}
   713  }
   714  
   715  func (i *integrationTest) fileNotExists(cID string, filePath string) {
   716  	res := i.fileExistsInContainer(cID, filePath)
   717  
   718  	if res {
   719  		i.t.Errorf("Unexpected file %s in container %s", filePath, cID)
   720  	}
   721  }
   722  
   723  func (i *integrationTest) runInImage(image string, cmd string) int {
   724  	return i.runInContainer(image, []string{"/bin/sh", "-c", cmd})
   725  }
   726  
   727  func (i *integrationTest) checkBasicBuildState(cID string, workingDir string) {
   728  	i.fileExists(cID, "/sti-fake/assemble-invoked")
   729  	i.fileExists(cID, "/sti-fake/run-invoked")
   730  	i.fileExists(cID, "/sti-fake/src/Gemfile")
   731  
   732  	_, err := os.Stat(workingDir)
   733  	if !os.IsNotExist(err) {
   734  		i.t.Errorf("Unexpected error from stat check on %s", workingDir)
   735  	}
   736  }
   737  
   738  func (i *integrationTest) checkIncrementalBuildState(cID string, workingDir string, expectClean bool) {
   739  	i.checkBasicBuildState(cID, workingDir)
   740  	if expectClean {
   741  		i.fileNotExists(cID, "/sti-fake/save-artifacts-invoked")
   742  	} else {
   743  		i.fileExists(cID, "/sti-fake/save-artifacts-invoked")
   744  	}
   745  }
   746  
   747  func (i *integrationTest) fileExistsInContainer(cID string, filePath string) bool {
   748  	ctx, cancel := getDefaultContext()
   749  	defer cancel()
   750  	rdr, stats, err := engineClient.CopyFromContainer(ctx, cID, filePath)
   751  	if err != nil {
   752  		return false
   753  	}
   754  	defer rdr.Close()
   755  	return "" != stats.Name
   756  }
   757  
   758  func (i *integrationTest) checkForLabel(image string) {
   759  	docker := dockerpkg.New(engineClient, (&api.Config{}).PullAuthentication)
   760  
   761  	labelMap, err := docker.GetLabels(image)
   762  	if err != nil {
   763  		i.t.Fatalf("Unable to get labels from image %s: %v", image, err)
   764  	}
   765  
   766  	if labelMap["testLabel"] != "testLabel_value" {
   767  		i.t.Errorf("Unable to verify 'testLabel' for image '%s'", image)
   768  	}
   769  }