at 23.11-pre 24 kB view raw
1# this test creates a simple GNU image with docker tools and sees if it executes 2 3import ./make-test-python.nix ({ pkgs, ... }: 4let 5 # nixpkgs#214434: dockerTools.buildImage fails to unpack base images 6 # containing duplicate layers when those duplicate tarballs 7 # appear under the manifest's 'Layers'. Docker can generate images 8 # like this even though dockerTools does not. 9 repeatedLayerTestImage = 10 let 11 # Rootfs diffs for layers 1 and 2 are identical (and empty) 12 layer1 = pkgs.dockerTools.buildImage { name = "empty"; }; 13 layer2 = layer1.overrideAttrs (_: { fromImage = layer1; }); 14 repeatedRootfsDiffs = pkgs.runCommandNoCC "image-with-links.tar" { 15 nativeBuildInputs = [pkgs.jq]; 16 } '' 17 mkdir contents 18 tar -xf "${layer2}" -C contents 19 cd contents 20 first_rootfs=$(jq -r '.[0].Layers[0]' manifest.json) 21 second_rootfs=$(jq -r '.[0].Layers[1]' manifest.json) 22 target_rootfs=$(sha256sum "$first_rootfs" | cut -d' ' -f 1).tar 23 24 # Replace duplicated rootfs diffs with symlinks to one tarball 25 chmod -R ug+w . 26 mv "$first_rootfs" "$target_rootfs" 27 rm "$second_rootfs" 28 ln -s "../$target_rootfs" "$first_rootfs" 29 ln -s "../$target_rootfs" "$second_rootfs" 30 31 # Update manifest's layers to use the symlinks' target 32 cat manifest.json | \ 33 jq ".[0].Layers[0] = \"$target_rootfs\"" | 34 jq ".[0].Layers[1] = \"$target_rootfs\"" > manifest.json.new 35 mv manifest.json.new manifest.json 36 37 tar --sort=name --hard-dereference -cf $out . 38 ''; 39 in pkgs.dockerTools.buildImage { 40 fromImage = repeatedRootfsDiffs; 41 name = "repeated-layer-test"; 42 tag = "latest"; 43 copyToRoot = pkgs.bash; 44 # A runAsRoot script is required to force previous layers to be unpacked 45 runAsRoot = '' 46 echo 'runAsRoot has run.' 47 ''; 48 }; 49in { 50 name = "docker-tools"; 51 meta = with pkgs.lib.maintainers; { 52 maintainers = [ lnl7 roberth ]; 53 }; 54 55 nodes = { 56 docker = { ... }: { 57 virtualisation = { 58 diskSize = 2048; 59 docker.enable = true; 60 }; 61 }; 62 }; 63 64 testScript = with pkgs.dockerTools; '' 65 unix_time_second1 = "1970-01-01T00:00:01Z" 66 67 docker.wait_for_unit("sockets.target") 68 69 with subtest("includeStorePath"): 70 with subtest("assumption"): 71 docker.succeed("${examples.helloOnRoot} | docker load") 72 docker.succeed("docker run --rm hello | grep -i hello") 73 docker.succeed("docker image rm hello:latest") 74 with subtest("includeStorePath = false; breaks example"): 75 docker.succeed("${examples.helloOnRootNoStore} | docker load") 76 docker.fail("docker run --rm hello | grep -i hello") 77 docker.succeed("docker image rm hello:latest") 78 with subtest("includeStorePath = false; works with mounted store"): 79 docker.succeed("${examples.helloOnRootNoStore} | docker load") 80 docker.succeed("docker run --rm --volume ${builtins.storeDir}:${builtins.storeDir}:ro hello | grep -i hello") 81 docker.succeed("docker image rm hello:latest") 82 83 with subtest("Ensure Docker images use a stable date by default"): 84 docker.succeed( 85 "docker load --input='${examples.bash}'" 86 ) 87 assert unix_time_second1 in docker.succeed( 88 "docker inspect ${examples.bash.imageName} " 89 + "| ${pkgs.jq}/bin/jq -r .[].Created", 90 ) 91 92 docker.succeed("docker run --rm ${examples.bash.imageName} bash --version") 93 # Check imageTag attribute matches image 94 docker.succeed("docker images --format '{{.Tag}}' | grep -F '${examples.bash.imageTag}'") 95 docker.succeed("docker rmi ${examples.bash.imageName}") 96 97 # The remaining combinations 98 with subtest("Ensure imageTag attribute matches image"): 99 docker.succeed( 100 "docker load --input='${examples.bashNoTag}'" 101 ) 102 docker.succeed( 103 "docker images --format '{{.Tag}}' | grep -F '${examples.bashNoTag.imageTag}'" 104 ) 105 docker.succeed("docker rmi ${examples.bashNoTag.imageName}:${examples.bashNoTag.imageTag}") 106 107 docker.succeed( 108 "docker load --input='${examples.bashNoTagLayered}'" 109 ) 110 docker.succeed( 111 "docker images --format '{{.Tag}}' | grep -F '${examples.bashNoTagLayered.imageTag}'" 112 ) 113 docker.succeed("docker rmi ${examples.bashNoTagLayered.imageName}:${examples.bashNoTagLayered.imageTag}") 114 115 docker.succeed( 116 "${examples.bashNoTagStreamLayered} | docker load" 117 ) 118 docker.succeed( 119 "docker images --format '{{.Tag}}' | grep -F '${examples.bashNoTagStreamLayered.imageTag}'" 120 ) 121 docker.succeed( 122 "docker rmi ${examples.bashNoTagStreamLayered.imageName}:${examples.bashNoTagStreamLayered.imageTag}" 123 ) 124 125 docker.succeed( 126 "docker load --input='${examples.nixLayered}'" 127 ) 128 docker.succeed("docker images --format '{{.Tag}}' | grep -F '${examples.nixLayered.imageTag}'") 129 docker.succeed("docker rmi ${examples.nixLayered.imageName}") 130 131 132 with subtest( 133 "Check if the nix store is correctly initialized by listing " 134 "dependencies of the installed Nix binary" 135 ): 136 docker.succeed( 137 "docker load --input='${examples.nix}'", 138 "docker run --rm ${examples.nix.imageName} nix-store -qR ${pkgs.nix}", 139 "docker rmi ${examples.nix.imageName}", 140 ) 141 142 with subtest( 143 "Ensure (layered) nix store has correct permissions " 144 "and that the container starts when its process does not have uid 0" 145 ): 146 docker.succeed( 147 "docker load --input='${examples.bashLayeredWithUser}'", 148 "docker run -u somebody --rm ${examples.bashLayeredWithUser.imageName} ${pkgs.bash}/bin/bash -c 'test 555 == $(stat --format=%a /nix) && test 555 == $(stat --format=%a /nix/store)'", 149 "docker rmi ${examples.bashLayeredWithUser.imageName}", 150 ) 151 152 with subtest("The nix binary symlinks are intact"): 153 docker.succeed( 154 "docker load --input='${examples.nix}'", 155 "docker run --rm ${examples.nix.imageName} ${pkgs.bash}/bin/bash -c 'test nix == $(readlink ${pkgs.nix}/bin/nix-daemon)'", 156 "docker rmi ${examples.nix.imageName}", 157 ) 158 159 with subtest("The nix binary symlinks are intact when the image is layered"): 160 docker.succeed( 161 "docker load --input='${examples.nixLayered}'", 162 "docker run --rm ${examples.nixLayered.imageName} ${pkgs.bash}/bin/bash -c 'test nix == $(readlink ${pkgs.nix}/bin/nix-daemon)'", 163 "docker rmi ${examples.nixLayered.imageName}", 164 ) 165 166 with subtest("The pullImage tool works"): 167 docker.succeed( 168 "docker load --input='${examples.testNixFromDockerHub}'", 169 "docker run --rm nix:2.2.1 nix-store --version", 170 "docker rmi nix:2.2.1", 171 ) 172 173 with subtest("runAsRoot and entry point work"): 174 docker.succeed( 175 "docker load --input='${examples.nginx}'", 176 "docker run --name nginx -d -p 8000:80 ${examples.nginx.imageName}", 177 ) 178 docker.wait_until_succeeds("curl -f http://localhost:8000/") 179 docker.succeed( 180 "docker rm --force nginx", 181 "docker rmi '${examples.nginx.imageName}'", 182 ) 183 184 with subtest("A pulled image can be used as base image"): 185 docker.succeed( 186 "docker load --input='${examples.onTopOfPulledImage}'", 187 "docker run --rm ontopofpulledimage hello", 188 "docker rmi ontopofpulledimage", 189 ) 190 191 with subtest("Regression test for issue #34779"): 192 docker.succeed( 193 "docker load --input='${examples.runAsRootExtraCommands}'", 194 "docker run --rm runasrootextracommands cat extraCommands", 195 "docker run --rm runasrootextracommands cat runAsRoot", 196 "docker rmi '${examples.runAsRootExtraCommands.imageName}'", 197 ) 198 199 with subtest("Ensure Docker images can use an unstable date"): 200 docker.succeed( 201 "docker load --input='${examples.unstableDate}'" 202 ) 203 assert unix_time_second1 not in docker.succeed( 204 "docker inspect ${examples.unstableDate.imageName} " 205 + "| ${pkgs.jq}/bin/jq -r .[].Created" 206 ) 207 208 with subtest("Ensure Layered Docker images can use an unstable date"): 209 docker.succeed( 210 "docker load --input='${examples.unstableDateLayered}'" 211 ) 212 assert unix_time_second1 not in docker.succeed( 213 "docker inspect ${examples.unstableDateLayered.imageName} " 214 + "| ${pkgs.jq}/bin/jq -r .[].Created" 215 ) 216 217 with subtest("Ensure Layered Docker images work"): 218 docker.succeed( 219 "docker load --input='${examples.layered-image}'", 220 "docker run --rm ${examples.layered-image.imageName}", 221 "docker run --rm ${examples.layered-image.imageName} cat extraCommands", 222 ) 223 224 with subtest("Ensure images built on top of layered Docker images work"): 225 docker.succeed( 226 "docker load --input='${examples.layered-on-top}'", 227 "docker run --rm ${examples.layered-on-top.imageName}", 228 ) 229 230 with subtest("Ensure layered images built on top of layered Docker images work"): 231 docker.succeed( 232 "docker load --input='${examples.layered-on-top-layered}'", 233 "docker run --rm ${examples.layered-on-top-layered.imageName}", 234 ) 235 236 237 def set_of_layers(image_name): 238 return set( 239 docker.succeed( 240 f"docker inspect {image_name} " 241 + "| ${pkgs.jq}/bin/jq -r '.[] | .RootFS.Layers | .[]'" 242 ).split() 243 ) 244 245 246 with subtest("Ensure layers are shared between images"): 247 docker.succeed( 248 "docker load --input='${examples.another-layered-image}'" 249 ) 250 layers1 = set_of_layers("${examples.layered-image.imageName}") 251 layers2 = set_of_layers("${examples.another-layered-image.imageName}") 252 assert bool(layers1 & layers2) 253 254 with subtest("Ensure order of layers is correct"): 255 docker.succeed( 256 "docker load --input='${examples.layersOrder}'" 257 ) 258 259 for index in 1, 2, 3: 260 assert f"layer{index}" in docker.succeed( 261 f"docker run --rm ${examples.layersOrder.imageName} cat /tmp/layer{index}" 262 ) 263 264 with subtest("Ensure layers unpacked in correct order before runAsRoot runs"): 265 assert "abc" in docker.succeed( 266 "docker load --input='${examples.layersUnpackOrder}'", 267 "docker run --rm ${examples.layersUnpackOrder.imageName} cat /layer-order" 268 ) 269 270 with subtest("Ensure repeated base layers handled by buildImage"): 271 docker.succeed( 272 "docker load --input='${repeatedLayerTestImage}'", 273 "docker run --rm ${repeatedLayerTestImage.imageName} /bin/bash -c 'exit 0'" 274 ) 275 276 with subtest("Ensure environment variables are correctly inherited"): 277 docker.succeed( 278 "docker load --input='${examples.environmentVariables}'" 279 ) 280 out = docker.succeed("docker run --rm ${examples.environmentVariables.imageName} env") 281 env = out.splitlines() 282 assert "FROM_PARENT=true" in env, "envvars from the parent should be preserved" 283 assert "FROM_CHILD=true" in env, "envvars from the child should be preserved" 284 assert "LAST_LAYER=child" in env, "envvars from the child should take priority" 285 286 with subtest("Ensure environment variables of layered images are correctly inherited"): 287 docker.succeed( 288 "docker load --input='${examples.environmentVariablesLayered}'" 289 ) 290 out = docker.succeed("docker run --rm ${examples.environmentVariablesLayered.imageName} env") 291 env = out.splitlines() 292 assert "FROM_PARENT=true" in env, "envvars from the parent should be preserved" 293 assert "FROM_CHILD=true" in env, "envvars from the child should be preserved" 294 assert "LAST_LAYER=child" in env, "envvars from the child should take priority" 295 296 with subtest( 297 "Ensure inherited environment variables of layered images are correctly resolved" 298 ): 299 # Read environment variables as stored in image config 300 config = docker.succeed( 301 "tar -xOf ${examples.environmentVariablesLayered} manifest.json | ${pkgs.jq}/bin/jq -r .[].Config" 302 ).strip() 303 out = docker.succeed( 304 f"tar -xOf ${examples.environmentVariablesLayered} {config} | ${pkgs.jq}/bin/jq -r '.config.Env | .[]'" 305 ) 306 env = out.splitlines() 307 assert ( 308 sum(entry.startswith("LAST_LAYER") for entry in env) == 1 309 ), "envvars overridden by child should be unique" 310 311 with subtest("Ensure image with only 2 layers can be loaded"): 312 docker.succeed( 313 "docker load --input='${examples.two-layered-image}'" 314 ) 315 316 with subtest( 317 "Ensure the bulk layer doesn't miss store paths (regression test for #78744)" 318 ): 319 docker.succeed( 320 "docker load --input='${pkgs.dockerTools.examples.bulk-layer}'", 321 # Ensure the two output paths (ls and hello) are in the layer 322 "docker run bulk-layer ls /bin/hello", 323 ) 324 325 with subtest( 326 "Ensure the bulk layer with a base image respects the number of maxLayers" 327 ): 328 docker.succeed( 329 "docker load --input='${pkgs.dockerTools.examples.layered-bulk-layer}'", 330 # Ensure the image runs correctly 331 "docker run layered-bulk-layer ls /bin/hello", 332 ) 333 334 # Ensure the image has the correct number of layers 335 assert len(set_of_layers("layered-bulk-layer")) == 4 336 337 with subtest("Ensure only minimal paths are added to the store"): 338 # TODO: make an example that has no store paths, for example by making 339 # busybox non-self-referential. 340 341 # This check tests that buildLayeredImage can build images that don't need a store. 342 docker.succeed( 343 "docker load --input='${pkgs.dockerTools.examples.no-store-paths}'" 344 ) 345 346 docker.succeed("docker run --rm no-store-paths ls / >/dev/console") 347 348 # If busybox isn't self-referential, we need this line 349 # docker.fail("docker run --rm no-store-paths ls /nix/store >/dev/console") 350 # However, it currently is self-referential, so we check that it is the 351 # only store path. 352 docker.succeed("diff <(docker run --rm no-store-paths ls /nix/store) <(basename ${pkgs.pkgsStatic.busybox}) >/dev/console") 353 354 with subtest("Ensure buildLayeredImage does not change store path contents."): 355 docker.succeed( 356 "docker load --input='${pkgs.dockerTools.examples.filesInStore}'", 357 "docker run --rm file-in-store nix-store --verify --check-contents", 358 "docker run --rm file-in-store |& grep 'some data'", 359 ) 360 361 with subtest("Ensure cross compiled image can be loaded and has correct arch."): 362 docker.succeed( 363 "docker load --input='${pkgs.dockerTools.examples.cross}'", 364 ) 365 assert ( 366 docker.succeed( 367 "docker inspect ${pkgs.dockerTools.examples.cross.imageName} " 368 + "| ${pkgs.jq}/bin/jq -r .[].Architecture" 369 ).strip() 370 == "${if pkgs.stdenv.hostPlatform.system == "aarch64-linux" then "amd64" else "arm64"}" 371 ) 372 373 with subtest("buildLayeredImage doesn't dereference /nix/store symlink layers"): 374 docker.succeed( 375 "docker load --input='${examples.layeredStoreSymlink}'", 376 "docker run --rm ${examples.layeredStoreSymlink.imageName} bash -c 'test -L ${examples.layeredStoreSymlink.passthru.symlink}'", 377 "docker rmi ${examples.layeredStoreSymlink.imageName}", 378 ) 379 380 with subtest("buildImage supports registry/ prefix in image name"): 381 docker.succeed( 382 "docker load --input='${examples.prefixedImage}'" 383 ) 384 docker.succeed( 385 "docker images --format '{{.Repository}}' | grep -F '${examples.prefixedImage.imageName}'" 386 ) 387 388 with subtest("buildLayeredImage supports registry/ prefix in image name"): 389 docker.succeed( 390 "docker load --input='${examples.prefixedLayeredImage}'" 391 ) 392 docker.succeed( 393 "docker images --format '{{.Repository}}' | grep -F '${examples.prefixedLayeredImage.imageName}'" 394 ) 395 396 with subtest("buildLayeredImage supports running chown with fakeRootCommands"): 397 docker.succeed( 398 "docker load --input='${examples.layeredImageWithFakeRootCommands}'" 399 ) 400 docker.succeed( 401 "docker run --rm ${examples.layeredImageWithFakeRootCommands.imageName} sh -c 'stat -c '%u' /home/alice | grep -E ^1000$'" 402 ) 403 404 with subtest("Ensure docker load on merged images loads all of the constituent images"): 405 docker.succeed( 406 "docker load --input='${examples.mergedBashAndRedis}'" 407 ) 408 docker.succeed( 409 "docker images --format '{{.Repository}}-{{.Tag}}' | grep -F '${examples.bash.imageName}-${examples.bash.imageTag}'" 410 ) 411 docker.succeed( 412 "docker images --format '{{.Repository}}-{{.Tag}}' | grep -F '${examples.redis.imageName}-${examples.redis.imageTag}'" 413 ) 414 docker.succeed("docker run --rm ${examples.bash.imageName} bash --version") 415 docker.succeed("docker run --rm ${examples.redis.imageName} redis-cli --version") 416 docker.succeed("docker rmi ${examples.bash.imageName}") 417 docker.succeed("docker rmi ${examples.redis.imageName}") 418 419 with subtest( 420 "Ensure docker load on merged images loads all of the constituent images (missing tags)" 421 ): 422 docker.succeed( 423 "docker load --input='${examples.mergedBashNoTagAndRedis}'" 424 ) 425 docker.succeed( 426 "docker images --format '{{.Repository}}-{{.Tag}}' | grep -F '${examples.bashNoTag.imageName}-${examples.bashNoTag.imageTag}'" 427 ) 428 docker.succeed( 429 "docker images --format '{{.Repository}}-{{.Tag}}' | grep -F '${examples.redis.imageName}-${examples.redis.imageTag}'" 430 ) 431 # we need to explicitly specify the generated tag here 432 docker.succeed( 433 "docker run --rm ${examples.bashNoTag.imageName}:${examples.bashNoTag.imageTag} bash --version" 434 ) 435 docker.succeed("docker run --rm ${examples.redis.imageName} redis-cli --version") 436 docker.succeed("docker rmi ${examples.bashNoTag.imageName}:${examples.bashNoTag.imageTag}") 437 docker.succeed("docker rmi ${examples.redis.imageName}") 438 439 with subtest("mergeImages preserves owners of the original images"): 440 docker.succeed( 441 "docker load --input='${examples.mergedBashFakeRoot}'" 442 ) 443 docker.succeed( 444 "docker run --rm ${examples.layeredImageWithFakeRootCommands.imageName} sh -c 'stat -c '%u' /home/alice | grep -E ^1000$'" 445 ) 446 447 with subtest("The image contains store paths referenced by the fakeRootCommands output"): 448 docker.succeed( 449 "docker run --rm ${examples.layeredImageWithFakeRootCommands.imageName} /hello/bin/layeredImageWithFakeRootCommands-hello" 450 ) 451 452 with subtest("exportImage produces a valid tarball"): 453 docker.succeed( 454 "tar -tf ${examples.exportBash} | grep '\./bin/bash' > /dev/null" 455 ) 456 457 with subtest("layered image fakeRootCommands with fakechroot works"): 458 docker.succeed("${examples.imageViaFakeChroot} | docker load") 459 docker.succeed("docker run --rm image-via-fake-chroot | grep -i hello") 460 docker.succeed("docker image rm image-via-fake-chroot:latest") 461 462 with subtest("Ensure bare paths in contents are loaded correctly"): 463 docker.succeed( 464 "docker load --input='${examples.build-image-with-path}'", 465 "docker run --rm build-image-with-path bash -c '[[ -e /hello.txt ]]'", 466 "docker rmi build-image-with-path", 467 ) 468 docker.succeed( 469 "${examples.layered-image-with-path} | docker load", 470 "docker run --rm layered-image-with-path bash -c '[[ -e /hello.txt ]]'", 471 "docker rmi layered-image-with-path", 472 ) 473 474 with subtest("Ensure correct architecture is present in manifests."): 475 docker.succeed(""" 476 docker load --input='${examples.build-image-with-architecture}' 477 docker inspect build-image-with-architecture \ 478 | ${pkgs.jq}/bin/jq -er '.[] | select(.Architecture=="arm64").Architecture' 479 docker rmi build-image-with-architecture 480 """) 481 docker.succeed(""" 482 ${examples.layered-image-with-architecture} | docker load 483 docker inspect layered-image-with-architecture \ 484 | ${pkgs.jq}/bin/jq -er '.[] | select(.Architecture=="arm64").Architecture' 485 docker rmi layered-image-with-architecture 486 """) 487 488 with subtest("etc"): 489 docker.succeed("${examples.etc} | docker load") 490 docker.succeed("docker run --rm etc | grep localhost") 491 docker.succeed("docker image rm etc:latest") 492 493 with subtest("image-with-certs"): 494 docker.succeed("<${examples.image-with-certs} docker load") 495 docker.succeed("docker run --rm image-with-certs:latest test -r /etc/ssl/certs/ca-bundle.crt") 496 docker.succeed("docker run --rm image-with-certs:latest test -r /etc/ssl/certs/ca-certificates.crt") 497 docker.succeed("docker run --rm image-with-certs:latest test -r /etc/pki/tls/certs/ca-bundle.crt") 498 docker.succeed("docker image rm image-with-certs:latest") 499 500 with subtest("buildNixShellImage: Can build a basic derivation"): 501 docker.succeed( 502 "${examples.nix-shell-basic} | docker load", 503 "docker run --rm nix-shell-basic bash -c 'buildDerivation && $out/bin/hello' | grep '^Hello, world!$'" 504 ) 505 506 with subtest("buildNixShellImage: Runs the shell hook"): 507 docker.succeed( 508 "${examples.nix-shell-hook} | docker load", 509 "docker run --rm -it nix-shell-hook | grep 'This is the shell hook!'" 510 ) 511 512 with subtest("buildNixShellImage: Sources stdenv, making build inputs available"): 513 docker.succeed( 514 "${examples.nix-shell-inputs} | docker load", 515 "docker run --rm -it nix-shell-inputs | grep 'Hello, world!'" 516 ) 517 518 with subtest("buildNixShellImage: passAsFile works"): 519 docker.succeed( 520 "${examples.nix-shell-pass-as-file} | docker load", 521 "docker run --rm -it nix-shell-pass-as-file | grep 'this is a string'" 522 ) 523 524 with subtest("buildNixShellImage: run argument works"): 525 docker.succeed( 526 "${examples.nix-shell-run} | docker load", 527 "docker run --rm -it nix-shell-run | grep 'This shell is not interactive'" 528 ) 529 530 with subtest("buildNixShellImage: command argument works"): 531 docker.succeed( 532 "${examples.nix-shell-command} | docker load", 533 "docker run --rm -it nix-shell-command | grep 'This shell is interactive'" 534 ) 535 536 with subtest("buildNixShellImage: home directory is writable by default"): 537 docker.succeed( 538 "${examples.nix-shell-writable-home} | docker load", 539 "docker run --rm -it nix-shell-writable-home" 540 ) 541 542 with subtest("buildNixShellImage: home directory can be made non-existent"): 543 docker.succeed( 544 "${examples.nix-shell-nonexistent-home} | docker load", 545 "docker run --rm -it nix-shell-nonexistent-home" 546 ) 547 548 with subtest("buildNixShellImage: can build derivations"): 549 docker.succeed( 550 "${examples.nix-shell-build-derivation} | docker load", 551 "docker run --rm -it nix-shell-build-derivation" 552 ) 553 ''; 554})