github.com/YousefHaggyHeroku/pack@v1.5.5/internal/build/phase_test.go (about)

     1  package build_test
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"math/rand"
     9  	"os"
    10  	"path/filepath"
    11  	"regexp"
    12  	"runtime"
    13  	"strconv"
    14  	"testing"
    15  	"time"
    16  
    17  	"github.com/buildpacks/imgutil/local"
    18  	"github.com/buildpacks/lifecycle/auth"
    19  	"github.com/docker/docker/api/types/filters"
    20  	"github.com/docker/docker/client"
    21  	"github.com/google/go-containerregistry/pkg/authn"
    22  	"github.com/heroku/color"
    23  	"github.com/sclevine/spec"
    24  	"github.com/sclevine/spec/report"
    25  
    26  	"github.com/YousefHaggyHeroku/pack/internal/archive"
    27  	"github.com/YousefHaggyHeroku/pack/internal/build"
    28  	"github.com/YousefHaggyHeroku/pack/internal/build/fakes"
    29  	ilogging "github.com/YousefHaggyHeroku/pack/internal/logging"
    30  	"github.com/YousefHaggyHeroku/pack/logging"
    31  	h "github.com/YousefHaggyHeroku/pack/testhelpers"
    32  )
    33  
    34  const phaseName = "phase"
    35  
    36  var (
    37  	repoName  string
    38  	ctrClient client.CommonAPIClient
    39  )
    40  
    41  // TestPhase is a integration test suite to ensure that the phase options are propagated to the container.
    42  func TestPhase(t *testing.T) {
    43  	rand.Seed(time.Now().UTC().UnixNano())
    44  
    45  	color.Disable(true)
    46  	defer color.Disable(false)
    47  
    48  	h.RequireDocker(t)
    49  
    50  	var err error
    51  	ctrClient, err = client.NewClientWithOpts(client.FromEnv, client.WithVersion("1.38"))
    52  	h.AssertNil(t, err)
    53  
    54  	info, err := ctrClient.Info(context.TODO())
    55  	h.AssertNil(t, err)
    56  	h.SkipIf(t, info.OSType == "windows", "These tests are not yet compatible with Windows-based containers")
    57  
    58  	repoName = "phase.test.lc-" + h.RandString(10)
    59  	wd, err := os.Getwd()
    60  	h.AssertNil(t, err)
    61  	h.CreateImageFromDir(t, ctrClient, repoName, filepath.Join(wd, "testdata", "fake-lifecycle"))
    62  	defer h.DockerRmi(ctrClient, repoName)
    63  
    64  	spec.Run(t, "phase", testPhase, spec.Report(report.Terminal{}), spec.Sequential())
    65  }
    66  
    67  func testPhase(t *testing.T, when spec.G, it spec.S) {
    68  	var (
    69  		lifecycleExec  *build.LifecycleExecution
    70  		phaseFactory   build.PhaseFactory
    71  		outBuf, errBuf bytes.Buffer
    72  		docker         client.CommonAPIClient
    73  		logger         logging.Logger
    74  		osType         string
    75  	)
    76  
    77  	it.Before(func() {
    78  		logger = ilogging.NewLogWithWriters(&outBuf, &outBuf)
    79  
    80  		var err error
    81  		docker, err = client.NewClientWithOpts(client.FromEnv, client.WithVersion("1.38"))
    82  		h.AssertNil(t, err)
    83  
    84  		info, err := ctrClient.Info(context.Background())
    85  		h.AssertNil(t, err)
    86  		osType = info.OSType
    87  
    88  		lifecycleExec, err = CreateFakeLifecycleExecution(logger, docker, filepath.Join("testdata", "fake-app"), repoName)
    89  		h.AssertNil(t, err)
    90  		phaseFactory = build.NewDefaultPhaseFactory(lifecycleExec)
    91  	})
    92  
    93  	it.After(func() {
    94  		h.AssertNil(t, lifecycleExec.Cleanup())
    95  	})
    96  
    97  	when("Phase", func() {
    98  		when("#Run", func() {
    99  			it("runs the subject phase on the builder image", func() {
   100  				configProvider := build.NewPhaseConfigProvider(phaseName, lifecycleExec)
   101  				phase := phaseFactory.New(configProvider)
   102  				assertRunSucceeds(t, phase, &outBuf, &errBuf)
   103  				h.AssertContains(t, outBuf.String(), "running some-lifecycle-phase")
   104  			})
   105  
   106  			it("prefixes the output with the phase name", func() {
   107  				configProvider := build.NewPhaseConfigProvider(phaseName, lifecycleExec, build.WithLogPrefix("phase"))
   108  				phase := phaseFactory.New(configProvider)
   109  				assertRunSucceeds(t, phase, &outBuf, &errBuf)
   110  				h.AssertContains(t, outBuf.String(), "[phase] running some-lifecycle-phase")
   111  			})
   112  
   113  			it("attaches the same layers volume to each phase", func() {
   114  				configProvider := build.NewPhaseConfigProvider(phaseName, lifecycleExec, build.WithArgs("write", "/layers/test.txt", "test-layers"))
   115  				writePhase := phaseFactory.New(configProvider)
   116  
   117  				assertRunSucceeds(t, writePhase, &outBuf, &errBuf)
   118  				h.AssertContains(t, outBuf.String(), "write test")
   119  
   120  				configProvider = build.NewPhaseConfigProvider(phaseName, lifecycleExec, build.WithArgs("read", "/layers/test.txt"))
   121  				readPhase := phaseFactory.New(configProvider)
   122  				assertRunSucceeds(t, readPhase, &outBuf, &errBuf)
   123  				h.AssertContains(t, outBuf.String(), "file contents: test-layers")
   124  			})
   125  
   126  			it("attaches the same app volume to each phase", func() {
   127  				configProvider := build.NewPhaseConfigProvider(phaseName, lifecycleExec, build.WithArgs("write", "/workspace/test.txt", "test-app"))
   128  				writePhase := phaseFactory.New(configProvider)
   129  				assertRunSucceeds(t, writePhase, &outBuf, &errBuf)
   130  				h.AssertContains(t, outBuf.String(), "write test")
   131  
   132  				configProvider = build.NewPhaseConfigProvider(phaseName, lifecycleExec, build.WithArgs("read", "/workspace/test.txt"))
   133  				readPhase := phaseFactory.New(configProvider)
   134  				assertRunSucceeds(t, readPhase, &outBuf, &errBuf)
   135  				h.AssertContains(t, outBuf.String(), "file contents: test-app")
   136  			})
   137  
   138  			it("copies the app into the app volume", func() {
   139  				configProvider := build.NewPhaseConfigProvider(
   140  					phaseName,
   141  					lifecycleExec,
   142  					build.WithArgs("read", "/workspace/fake-app-file"),
   143  					build.WithContainerOperations(
   144  						build.CopyDir(
   145  							lifecycleExec.AppPath(),
   146  							"/workspace",
   147  							lifecycleExec.Builder().UID(),
   148  							lifecycleExec.Builder().GID(),
   149  							osType,
   150  							nil,
   151  						),
   152  					),
   153  				)
   154  				readPhase := phaseFactory.New(configProvider)
   155  				assertRunSucceeds(t, readPhase, &outBuf, &errBuf)
   156  				h.AssertContains(t, outBuf.String(), "file contents: fake-app-contents")
   157  				h.AssertContains(t, outBuf.String(), "file uid/gid: 111/222")
   158  
   159  				configProvider = build.NewPhaseConfigProvider(phaseName, lifecycleExec, build.WithArgs("delete", "/workspace/fake-app-file"))
   160  				deletePhase := phaseFactory.New(configProvider)
   161  				assertRunSucceeds(t, deletePhase, &outBuf, &errBuf)
   162  				h.AssertContains(t, outBuf.String(), "delete test")
   163  
   164  				configProvider = build.NewPhaseConfigProvider(phaseName, lifecycleExec, build.WithArgs("read", "/workspace/fake-app-file"))
   165  				readPhase2 := phaseFactory.New(configProvider)
   166  				err := readPhase2.Run(context.TODO())
   167  				readPhase2.Cleanup()
   168  				h.AssertNotNil(t, err)
   169  				h.AssertContains(t, outBuf.String(), "failed to read file")
   170  			})
   171  
   172  			when("app is a dir", func() {
   173  				it("preserves original mod times", func() {
   174  					assertAppModTimePreserved(t, lifecycleExec, phaseFactory, &outBuf, &errBuf, osType)
   175  				})
   176  			})
   177  
   178  			when("app is a zip", func() {
   179  				it("preserves original mod times", func() {
   180  					var err error
   181  					lifecycleExec, err = CreateFakeLifecycleExecution(logger, docker, filepath.Join("testdata", "fake-app.zip"), repoName)
   182  					h.AssertNil(t, err)
   183  					phaseFactory = build.NewDefaultPhaseFactory(lifecycleExec)
   184  
   185  					assertAppModTimePreserved(t, lifecycleExec, phaseFactory, &outBuf, &errBuf, osType)
   186  				})
   187  			})
   188  
   189  			when("is posix", func() {
   190  				it.Before(func() {
   191  					h.SkipIf(t, runtime.GOOS == "windows", "Skipping on windows")
   192  				})
   193  
   194  				when("restricted directory is present", func() {
   195  					var (
   196  						err              error
   197  						tmpFakeAppDir    string
   198  						dirWithoutAccess string
   199  					)
   200  
   201  					it.Before(func() {
   202  						h.SkipIf(t, os.Getuid() == 0, "Skipping b/c current user is root")
   203  
   204  						tmpFakeAppDir, err = ioutil.TempDir("", "fake-app")
   205  						h.AssertNil(t, err)
   206  						dirWithoutAccess = filepath.Join(tmpFakeAppDir, "bad-dir")
   207  						err := os.MkdirAll(dirWithoutAccess, 0222)
   208  						h.AssertNil(t, err)
   209  					})
   210  
   211  					it.After(func() {
   212  						h.AssertNil(t, os.RemoveAll(tmpFakeAppDir))
   213  					})
   214  
   215  					it("returns an error", func() {
   216  						logger := ilogging.NewLogWithWriters(&outBuf, &outBuf)
   217  						lifecycleExec, err = CreateFakeLifecycleExecution(logger, docker, tmpFakeAppDir, repoName)
   218  						h.AssertNil(t, err)
   219  						phaseFactory = build.NewDefaultPhaseFactory(lifecycleExec)
   220  						readPhase := phaseFactory.New(build.NewPhaseConfigProvider(
   221  							phaseName,
   222  							lifecycleExec,
   223  							build.WithArgs("read", "/workspace/fake-app-file"),
   224  							build.WithContainerOperations(
   225  								build.CopyDir(lifecycleExec.AppPath(), "/workspace", 0, 0, osType, nil),
   226  							),
   227  						))
   228  						h.AssertNil(t, err)
   229  						err = readPhase.Run(context.TODO())
   230  						defer readPhase.Cleanup()
   231  
   232  						h.AssertNotNil(t, err)
   233  						h.AssertContains(t,
   234  							err.Error(),
   235  							fmt.Sprintf("open %s: permission denied", dirWithoutAccess),
   236  						)
   237  					})
   238  				})
   239  			})
   240  
   241  			it("sets the proxy vars in the container", func() {
   242  				configProvider := build.NewPhaseConfigProvider(phaseName, lifecycleExec, build.WithArgs("proxy"))
   243  				phase := phaseFactory.New(configProvider)
   244  				assertRunSucceeds(t, phase, &outBuf, &errBuf)
   245  				h.AssertContains(t, outBuf.String(), "HTTP_PROXY=some-http-proxy")
   246  				h.AssertContains(t, outBuf.String(), "HTTPS_PROXY=some-https-proxy")
   247  				h.AssertContains(t, outBuf.String(), "NO_PROXY=some-no-proxy")
   248  				h.AssertContains(t, outBuf.String(), "http_proxy=some-http-proxy")
   249  				h.AssertContains(t, outBuf.String(), "https_proxy=some-https-proxy")
   250  				h.AssertContains(t, outBuf.String(), "no_proxy=some-no-proxy")
   251  			})
   252  
   253  			when("#WithArgs", func() {
   254  				it("runs the subject phase with args", func() {
   255  					configProvider := build.NewPhaseConfigProvider(phaseName, lifecycleExec, build.WithArgs("some", "args"))
   256  					phase := phaseFactory.New(configProvider)
   257  					assertRunSucceeds(t, phase, &outBuf, &errBuf)
   258  					h.AssertContains(t, outBuf.String(), `received args [/cnb/lifecycle/phase some args]`)
   259  				})
   260  			})
   261  
   262  			when("#WithDaemonAccess", func() {
   263  				it("allows daemon access inside the container", func() {
   264  					configProvider := build.NewPhaseConfigProvider(phaseName, lifecycleExec, build.WithArgs("daemon"), build.WithDaemonAccess())
   265  					phase := phaseFactory.New(configProvider)
   266  					assertRunSucceeds(t, phase, &outBuf, &errBuf)
   267  					h.AssertContains(t, outBuf.String(), "daemon test")
   268  				})
   269  			})
   270  
   271  			when("#WithRoot", func() {
   272  				it("sets the containers user to root", func() {
   273  					configProvider := build.NewPhaseConfigProvider(phaseName, lifecycleExec, build.WithArgs("user"), build.WithRoot())
   274  					phase := phaseFactory.New(configProvider)
   275  					assertRunSucceeds(t, phase, &outBuf, &errBuf)
   276  					h.AssertContains(t, outBuf.String(), "current user is root")
   277  				})
   278  			})
   279  
   280  			when("#WithBinds", func() {
   281  				it.After(func() {
   282  					docker.VolumeRemove(context.TODO(), "some-volume", true)
   283  				})
   284  
   285  				it("mounts volumes inside container", func() {
   286  					configProvider := build.NewPhaseConfigProvider(phaseName, lifecycleExec, build.WithArgs("binds"), build.WithBinds("some-volume:/mounted"))
   287  					phase := phaseFactory.New(configProvider)
   288  					assertRunSucceeds(t, phase, &outBuf, &errBuf)
   289  					h.AssertContains(t, outBuf.String(), "binds test")
   290  					body, err := docker.VolumeList(context.TODO(), filters.NewArgs(filters.KeyValuePair{
   291  						Key:   "name",
   292  						Value: "some-volume",
   293  					}))
   294  					h.AssertNil(t, err)
   295  					h.AssertEq(t, len(body.Volumes), 1)
   296  				})
   297  			})
   298  
   299  			when("#WithRegistryAccess", func() {
   300  				var registry *h.TestRegistryConfig
   301  
   302  				it.Before(func() {
   303  					registry = h.RunRegistry(t)
   304  					h.AssertNil(t, os.Setenv("DOCKER_CONFIG", registry.DockerConfigDir))
   305  				})
   306  
   307  				it.After(func() {
   308  					if registry != nil {
   309  						registry.StopRegistry(t)
   310  					}
   311  					h.AssertNil(t, os.Unsetenv("DOCKER_CONFIG"))
   312  				})
   313  
   314  				it("provides auth for registry in the container", func() {
   315  					repoName := h.CreateImageOnRemote(t, ctrClient, registry, "packs/build:v3alpha2", "FROM busybox")
   316  
   317  					authConfig, err := auth.BuildEnvVar(authn.DefaultKeychain, repoName)
   318  					h.AssertNil(t, err)
   319  
   320  					configProvider := build.NewPhaseConfigProvider(
   321  						phaseName,
   322  						lifecycleExec,
   323  						build.WithArgs("registry", repoName),
   324  						build.WithRegistryAccess(authConfig),
   325  						build.WithNetwork("host"),
   326  					)
   327  					phase := phaseFactory.New(configProvider)
   328  					assertRunSucceeds(t, phase, &outBuf, &errBuf)
   329  					h.AssertContains(t, outBuf.String(), "registry test")
   330  				})
   331  			})
   332  
   333  			when("#WithNetwork", func() {
   334  				it("specifies a network for the container", func() {
   335  					configProvider := build.NewPhaseConfigProvider(phaseName, lifecycleExec, build.WithArgs("network"), build.WithNetwork("none"))
   336  					phase := phaseFactory.New(configProvider)
   337  					assertRunSucceeds(t, phase, &outBuf, &errBuf)
   338  					h.AssertNotContainsMatch(t, outBuf.String(), `interface: eth\d+`)
   339  					h.AssertContains(t, outBuf.String(), `error connecting to internet:`)
   340  				})
   341  			})
   342  		})
   343  	})
   344  
   345  	when("#Cleanup", func() {
   346  		it.Before(func() {
   347  			configProvider := build.NewPhaseConfigProvider(phaseName, lifecycleExec)
   348  			phase := phaseFactory.New(configProvider)
   349  			assertRunSucceeds(t, phase, &outBuf, &errBuf)
   350  			h.AssertContains(t, outBuf.String(), "running some-lifecycle-phase")
   351  
   352  			h.AssertNil(t, lifecycleExec.Cleanup())
   353  		})
   354  
   355  		it("should delete the layers volume", func() {
   356  			body, err := docker.VolumeList(context.TODO(),
   357  				filters.NewArgs(filters.KeyValuePair{
   358  					Key:   "name",
   359  					Value: lifecycleExec.LayersVolume(),
   360  				}))
   361  			h.AssertNil(t, err)
   362  			h.AssertEq(t, len(body.Volumes), 0)
   363  		})
   364  
   365  		it("should delete the app volume", func() {
   366  			body, err := docker.VolumeList(context.TODO(),
   367  				filters.NewArgs(filters.KeyValuePair{
   368  					Key:   "name",
   369  					Value: lifecycleExec.AppVolume(),
   370  				}))
   371  			h.AssertNil(t, err)
   372  			h.AssertEq(t, len(body.Volumes), 0)
   373  		})
   374  	})
   375  }
   376  
   377  func assertAppModTimePreserved(t *testing.T, lifecycle *build.LifecycleExecution, phaseFactory build.PhaseFactory, outBuf *bytes.Buffer, errBuf *bytes.Buffer, osType string) {
   378  	t.Helper()
   379  	readPhase := phaseFactory.New(build.NewPhaseConfigProvider(
   380  		phaseName,
   381  		lifecycle,
   382  		build.WithArgs("read", "/workspace/fake-app-file"),
   383  		build.WithContainerOperations(
   384  			build.CopyDir(lifecycle.AppPath(), "/workspace", 0, 0, osType, nil),
   385  		),
   386  	))
   387  	assertRunSucceeds(t, readPhase, outBuf, errBuf)
   388  
   389  	matches := regexp.MustCompile(regexp.QuoteMeta("file mod time (unix): ") + "(.*)").FindStringSubmatch(outBuf.String())
   390  	h.AssertEq(t, len(matches), 2)
   391  	h.AssertFalse(t, matches[1] == strconv.FormatInt(archive.NormalizedDateTime.Unix(), 10))
   392  }
   393  
   394  func assertRunSucceeds(t *testing.T, phase build.RunnerCleaner, outBuf *bytes.Buffer, errBuf *bytes.Buffer) {
   395  	t.Helper()
   396  	if err := phase.Run(context.TODO()); err != nil {
   397  		phase.Cleanup()
   398  		t.Fatalf("Failed to run phase: %s\nstdout:\n%s\nstderr:\n%s\n", err, outBuf.String(), errBuf.String())
   399  	}
   400  	phase.Cleanup()
   401  }
   402  
   403  func CreateFakeLifecycleExecution(logger logging.Logger, docker client.CommonAPIClient, appDir string, repoName string) (*build.LifecycleExecution, error) {
   404  	builderImage, err := local.NewImage(repoName, docker, local.FromBaseImage(repoName))
   405  	if err != nil {
   406  		return nil, err
   407  	}
   408  
   409  	fakeBuilder, err := fakes.NewFakeBuilder(
   410  		fakes.WithUID(111), fakes.WithGID(222),
   411  		fakes.WithImage(builderImage),
   412  	)
   413  	if err != nil {
   414  		return nil, err
   415  	}
   416  
   417  	return build.NewLifecycleExecution(logger, docker, build.LifecycleOptions{
   418  		AppPath:    appDir,
   419  		Builder:    fakeBuilder,
   420  		HTTPProxy:  "some-http-proxy",
   421  		HTTPSProxy: "some-https-proxy",
   422  		NoProxy:    "some-no-proxy",
   423  	})
   424  }