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 }